├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── README.rst ├── dev-requirements.txt ├── doac ├── __init__.py ├── admin.py ├── compat.py ├── conf.py ├── contrib │ ├── __init__.py │ └── rest_framework │ │ ├── __init__.py │ │ ├── authentication.py │ │ └── permissions.py ├── decorators.py ├── exceptions │ ├── __init__.py │ ├── access_denied.py │ ├── base.py │ ├── insufficient_scope.py │ ├── invalid_client.py │ ├── invalid_request.py │ ├── invalid_scope.py │ ├── unsupported_grant_type.py │ └── unsupported_response_type.py ├── forms.py ├── handlers │ ├── __init__.py │ └── bearer.py ├── http.py ├── managers.py ├── middleware.py ├── models.py ├── templates │ └── doac │ │ ├── authorize.html │ │ ├── base.html │ │ └── internal_app.html ├── urls.py ├── utils.py └── views.py ├── docs ├── api.md ├── exceptions │ ├── base.md │ ├── index.md │ ├── invalid_client.md │ ├── invalid_request.md │ ├── invalid_scope.md │ └── unsupported.md ├── index.md ├── installation.md ├── integrations.md ├── markdown │ ├── api.md │ ├── exceptions │ │ ├── base.md │ │ ├── index.md │ │ ├── invalid_client.md │ │ ├── invalid_request.md │ │ ├── invalid_scope.md │ │ └── unsupported.md │ ├── index.md │ ├── installation.md │ ├── integrations.md │ ├── models │ │ └── index.md │ ├── settings.md │ └── utilities.md ├── models │ └── index.md ├── settings.md └── utilities.md ├── requirements.txt ├── runtests.py ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── models.py ├── settings.py ├── tests ├── __init__.py ├── contrib │ ├── __init__.py │ └── test_rest_framework.py ├── handlers │ ├── __init__.py │ └── test_bearer.py ├── integrations │ ├── __init__.py │ ├── test_idan_oauthlib.py │ └── test_litl_rauth.py ├── mock.py ├── models │ ├── __init__.py │ ├── test_authorization_token.py │ ├── test_client.py │ └── test_redirect_uri.py ├── test_cases.py ├── test_decorators.py ├── test_middleware.py └── views │ ├── __init__.py │ ├── test_approval.py │ ├── test_authorize.py │ └── test_token.py ├── urls.py └── views.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | htmlcov 29 | 30 | # Translations 31 | *.mo 32 | 33 | # SASS Cache Files 34 | .sass-cache 35 | 36 | # Temporary Files 37 | tmp 38 | 39 | # Unrelated Django files 40 | website 41 | manage.py 42 | 43 | # PyCharm 44 | 45 | .idea 46 | *~ 47 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "2.6" 5 | - "2.7" 6 | 7 | env: 8 | - DJANGO=Django==1.3.7 9 | - DJANGO=Django==1.4.10 10 | - DJANGO=Django==1.5.5 11 | - DJANGO=Django==1.6.1 12 | - DJANGO=https://github.com/django/django/tarball/master 13 | 14 | matrix: 15 | exclude: 16 | - env: DJANGO=https://github.com/django/django/tarball/master 17 | python: "2.6" 18 | allow_failures: 19 | - env: DJANGO=https://github.com/django/django/tarball/master 20 | 21 | install: 22 | - "pip install -q $DJANGO --use-mirrors" 23 | - "pip install argparse" 24 | - "pip install pep8" 25 | - "python setup.py install" 26 | - "pip install -r dev-requirements.txt" 27 | 28 | before_script: 29 | - "pep8 doac" 30 | 31 | script: 32 | - "python runtests.py --no-coverage" 33 | 34 | notifications: 35 | email: false 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2013 Kevin Brown 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django OAuth2 Consumer (doac) [![Build Status](https://travis-ci.org/Rediker-Software/doac.png?branch=master)](https://travis-ci.org/Rediker-Software/doac) 2 | 3 | DOAC is a reusable application that can be used to provide an OAuth2 cosumer for your project. It is based on [RFC 6749](http://tools.ietf.org/html/rfc6749) for the [OAuth2 authorization framework](http://oauth.net/2/). 4 | 5 | ## What do I need to use this? 6 | - Django 1.3+ 7 | - Django [authentication application](https://docs.djangoproject.com/en/1.5/topics/auth/) 8 | - Python 2.6+ 9 | 10 | This plugin has not been tested on other configurations. If it works with different requirements, or if a requirement is missing from the list, feel free to bring up an issue. 11 | 12 | ## What else does this have support for? 13 | - Django [admin application](https://docs.djangoproject.com/en/1.5/ref/contrib/admin/) 14 | - [Django Rest Framework](http://django-rest-framework.org/) - [Instructions](docs/integrations.md) 15 | 16 | ## Where is the documentation? 17 | The documentation is not complete, but we try our best to keep them current and comprehensive. 18 | 19 | They are included in this repository in markdown versions. [You can view them here.](docs/index.md) 20 | 21 | ## Where are the tests? 22 | We are trying our best to keep them up to date. Feel free to submit a pull request with tests for code that is not covered. 23 | 24 | You can run the tests with: 25 | ``` 26 | python runtests.py 27 | ``` 28 | The test runner has support for coverage.py along with some other options. 29 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Django OAuth2 Consumer (doac) |Build Status| 2 | ============================================ 3 | 4 | django-oauth2-consumer (DOAC) is a reusable application that can be used 5 | to provide an OAuth2 cosumer for your project. It is based on `RFC 6 | 6749 `__ for the `OAuth2 7 | authorization framework `__. 8 | 9 | This project is in active development in the alpha stage. It is not 10 | recommended for use in production and is not complete. 11 | 12 | What do I need to use this? 13 | --------------------------- 14 | 15 | - Django 1.3+ 16 | - Django `authentication 17 | application `__ 18 | - Python 2.6+ 19 | 20 | This plugin has not been tested on other configurations. If it works 21 | with different requirements, or if a requirement is missing from the 22 | list, feel free to bring up an issue. 23 | 24 | What else does this have support for? 25 | ------------------------------------- 26 | 27 | - Django `admin 28 | application `__ 29 | 30 | Where is the documentation? 31 | --------------------------- 32 | 33 | The documentation is not complete, but we try our best to keep them 34 | current and comprehensive. 35 | 36 | `You can view them here on 37 | ReadTheDocs. `__ 38 | 39 | Where are the tests? 40 | -------------------- 41 | 42 | We are trying our best to keep them up to date. Feel free to submit a 43 | pull request with tests for code that is missing it. 44 | 45 | | You can run the tests with: 46 | | ``python runtests.py`` 47 | | The test runner has support for coverage.py along with some other 48 | options. 49 | 50 | .. |Build Status| image:: https://travis-ci.org/kevin-brown/doac.png?branch=master 51 | :target: https://travis-ci.org/kevin-brown/doac 52 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | Django 2 | pep8 3 | 4 | djangorestframework 5 | pyoauth2 6 | oauthlib 7 | rauth 8 | -------------------------------------------------------------------------------- /doac/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.2.0" 2 | -------------------------------------------------------------------------------- /doac/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import AccessToken, AuthorizationCode, AuthorizationToken, Client, RedirectUri, RefreshToken, Scope 3 | 4 | 5 | class AccessTokenAdmin(admin.ModelAdmin): 6 | list_display = ("client", "user", "truncated_refresh_token", "truncated_token", ) 7 | list_filter = ("created_at", "expires_at", "is_active", ) 8 | 9 | def truncated_refresh_token(self, obj): 10 | return obj.refresh_token.token[0:40] + "..." 11 | truncated_refresh_token.short_description = "refresh token" 12 | 13 | def truncated_token(self, obj): 14 | return obj.token[0:40] + "..." 15 | truncated_token.short_description = "token" 16 | 17 | 18 | class AuthorizationCodeAdmin(admin.ModelAdmin): 19 | list_display = ("client", "redirect_uri", "truncated_token", ) 20 | list_filter = ("created_at", "expires_at", "is_active", ) 21 | 22 | def truncated_token(self, obj): 23 | return obj.token[0:50] + "..." 24 | truncated_token.short_description = "token" 25 | 26 | 27 | class AuthorizationTokenAdmin(admin.ModelAdmin): 28 | list_display = ("client", "user", "truncated_token") 29 | list_filter = ("created_at", "expires_at", "is_active", ) 30 | 31 | def truncated_token(self, obj): 32 | return obj.token[0:75] + "..." 33 | truncated_token.short_description = "token" 34 | 35 | 36 | class ClientAdmin(admin.ModelAdmin): 37 | list_display = ("name", "secret", "access_host", ) 38 | list_filter = ("is_active", ) 39 | 40 | 41 | class RedirectUriAdmin(admin.ModelAdmin): 42 | list_display = ("client", "url", ) 43 | 44 | 45 | class RefreshTokenAdmin(admin.ModelAdmin): 46 | list_display = ("client", "user", "truncated_authorization_token", "truncated_token", ) 47 | list_filter = ("created_at", "expires_at", "is_active", ) 48 | 49 | def truncated_authorization_token(self, obj): 50 | return obj.authorization_token.token[0:40] + "..." 51 | truncated_authorization_token.short_description = "token" 52 | 53 | def truncated_token(self, obj): 54 | return obj.token[0:40] + "..." 55 | truncated_token.short_description = "token" 56 | 57 | 58 | class ScopeAdmin(admin.ModelAdmin): 59 | list_display = ("short_name", "full_name", "description", ) 60 | 61 | 62 | admin.site.register(AccessToken, AccessTokenAdmin) 63 | admin.site.register(AuthorizationCode, AuthorizationCodeAdmin) 64 | admin.site.register(AuthorizationToken, AuthorizationTokenAdmin) 65 | admin.site.register(Client, ClientAdmin) 66 | admin.site.register(RedirectUri, RedirectUriAdmin) 67 | admin.site.register(RefreshToken, RefreshTokenAdmin) 68 | admin.site.register(Scope, ScopeAdmin) 69 | -------------------------------------------------------------------------------- /doac/compat.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | try: 4 | random = random.SystemRandom() 5 | except NotImplementedError: 6 | pass 7 | 8 | 9 | def get_random_string(length=12, allowed_chars='abcdefghijklmnopqrstuvwxyz' 10 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 11 | '0123456789'): 12 | return ''.join([random.choice(allowed_chars) for i in range(length)]) 13 | 14 | 15 | def get_user_model(): 16 | """ 17 | Get the user model that is being used. If the `get_user_model` method 18 | is not available, default back to the standard User model provided 19 | through `django.contrib.auth`. 20 | """ 21 | 22 | try: 23 | from django.contrib.auth import get_user_model 24 | 25 | return get_user_model() 26 | except ImportError: 27 | from django.contrib.auth.models import User 28 | 29 | return User 30 | 31 | try: 32 | from django.utils import timezone 33 | 34 | now = timezone.now 35 | except ImportError: 36 | from datetime import datetime 37 | 38 | now = datetime.now 39 | -------------------------------------------------------------------------------- /doac/conf.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | import datetime 3 | 4 | 5 | class Settings: 6 | 7 | def __init__(self, options): 8 | self.handlers = options.get("HANDLERS", None) 9 | 10 | if not self.handlers: 11 | self.handlers = ( 12 | "doac.handlers.bearer.BearerHandler", 13 | ) 14 | 15 | self.realm = options.get("REALM", "oauth") 16 | 17 | self.access_token = options.get("ACCESS_TOKEN", {}) 18 | self.setup_access_token() 19 | 20 | self.auth_code = options.get("AUTHORIZATION_CODE", {}) 21 | self.setup_auth_code() 22 | 23 | self.auth_token = options.get("AUTHORIZATION_TOKEN", {}) 24 | self.setup_auth_token() 25 | 26 | self.client = options.get("CLIENT", {}) 27 | self.setup_client() 28 | 29 | self.refresh_token = options.get("REFRESH_TOKEN", {}) 30 | self.setup_refresh_token() 31 | 32 | def setup_access_token(self): 33 | at = self.access_token 34 | token = {} 35 | 36 | token["expires"] = at.get("EXPIRES", datetime.timedelta(hours=2)) 37 | token["length"] = at.get("LENGTH", 100) 38 | 39 | self.access_token = token 40 | 41 | def setup_auth_code(self): 42 | ac = self.auth_code 43 | token = {} 44 | 45 | token["expires"] = ac.get("EXPIRES", datetime.timedelta(minutes=15)) 46 | token["length"] = ac.get("LENGTH", 100) 47 | 48 | self.auth_code = token 49 | 50 | def setup_auth_token(self): 51 | at = self.auth_token 52 | token = {} 53 | 54 | token["expires"] = at.get("EXPIRES", datetime.timedelta(minutes=15)) 55 | token["length"] = at.get("LENGTH", 100) 56 | 57 | self.auth_token = token 58 | 59 | def setup_client(self): 60 | cli = self.client 61 | client = {} 62 | 63 | client["length"] = cli.get("LENGTH", 50) 64 | 65 | self.client = client 66 | 67 | def setup_refresh_token(self): 68 | rt = self.refresh_token 69 | token = {} 70 | 71 | token["expires"] = rt.get("EXPIRES", datetime.timedelta(days=60)) 72 | token["length"] = rt.get("LENGTH", 100) 73 | 74 | self.refresh_token = token 75 | 76 | options_dict = getattr(settings, "OAUTH_CONFIG", {}) 77 | options = Settings(options_dict) 78 | -------------------------------------------------------------------------------- /doac/contrib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rediker-Software/doac/398fdd64452e4ff8662297b0381926addd77505a/doac/contrib/__init__.py -------------------------------------------------------------------------------- /doac/contrib/rest_framework/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rediker-Software/doac/398fdd64452e4ff8662297b0381926addd77505a/doac/contrib/rest_framework/__init__.py -------------------------------------------------------------------------------- /doac/contrib/rest_framework/authentication.py: -------------------------------------------------------------------------------- 1 | from rest_framework import authentication, exceptions 2 | 3 | 4 | class DoacAuthentication(authentication.BaseAuthentication): 5 | 6 | def authenticate(self, request): 7 | """ 8 | Send the request through the authentication middleware that 9 | is provided with DOAC and grab the user and token from it. 10 | """ 11 | 12 | from doac.middleware import AuthenticationMiddleware 13 | 14 | try: 15 | response = AuthenticationMiddleware().process_request(request) 16 | except: 17 | raise exceptions.AuthenticationFailed("Invalid handler") 18 | 19 | if not hasattr(request, "user") or not request.user.is_authenticated(): 20 | return None 21 | 22 | if not hasattr(request, "access_token"): 23 | raise exceptions.AuthenticationFailed("Access token was not valid") 24 | 25 | return request.user, request.access_token 26 | 27 | def authenticate_header(self, request): 28 | """ 29 | DOAC specifies the realm as Bearer by default. 30 | """ 31 | 32 | from doac.conf import options 33 | 34 | return 'Bearer realm="%s"' % options.realm 35 | -------------------------------------------------------------------------------- /doac/contrib/rest_framework/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework import permissions 2 | 3 | 4 | class TokenHasScope(permissions.BasePermission): 5 | 6 | def has_permission(self, request, view): 7 | from doac.decorators import scope_required 8 | 9 | scopes = getattr(view, "scopes", []) 10 | 11 | if not hasattr(request, "auth") or not request.auth: 12 | return False 13 | 14 | request.access_token = request.auth 15 | 16 | @scope_required(*scopes) 17 | def test_func(request): 18 | return "Pass" 19 | 20 | response = test_func(request) 21 | 22 | if response == "Pass": 23 | return True 24 | 25 | return False 26 | -------------------------------------------------------------------------------- /doac/decorators.py: -------------------------------------------------------------------------------- 1 | from django.utils.decorators import available_attrs 2 | from functools import wraps 3 | from .exceptions.invalid_request import CredentialsNotProvided 4 | from .exceptions.insufficient_scope import ScopeNotEnough 5 | 6 | 7 | def scope_required(*scopes): 8 | """ 9 | Test for specific scopes that the access token has been authenticated for before 10 | processing the request and eventual response. 11 | 12 | The scopes that are passed in determine how the decorator will respond to incoming 13 | requests: 14 | 15 | - If no scopes are passed in the arguments, the decorator will test for any available 16 | scopes and determine the response based on that. 17 | 18 | - If specific scopes are passed, the access token will be checked to make sure it has 19 | all of the scopes that were requested. 20 | 21 | This decorator will change the response if the access toke does not have the scope: 22 | 23 | - If an invalid scope is requested (one that does not exist), all requests will be 24 | denied, as no access tokens will be able to fulfill the scope request and the 25 | request will be denied. 26 | 27 | - If the access token does not have one of the requested scopes, the request will be 28 | denied and the user will be returned one of two responses: 29 | 30 | - A 400 response (Bad Request) will be returned if an unauthenticated user tries to 31 | access the resource. 32 | 33 | - A 403 response (Forbidden) will be returned if an authenticated user ties to access 34 | the resource but does not have the correct scope. 35 | """ 36 | 37 | def decorator(view_func): 38 | 39 | @wraps(view_func, assigned=available_attrs(view_func)) 40 | def _wrapped_view(request, *args, **kwargs): 41 | from django.http import HttpResponseBadRequest, HttpResponseForbidden 42 | from .exceptions.base import InvalidRequest, InsufficientScope 43 | from .models import Scope 44 | from .utils import request_error_header 45 | 46 | try: 47 | if not hasattr(request, "access_token"): 48 | raise CredentialsNotProvided() 49 | 50 | access_token = request.access_token 51 | 52 | for scope_name in scopes: 53 | try: 54 | scope = access_token.scope.for_short_name(scope_name) 55 | except Scope.DoesNotExist: 56 | raise ScopeNotEnough() 57 | except InvalidRequest as e: 58 | response = HttpResponseBadRequest() 59 | response["WWW-Authenticate"] = request_error_header(e) 60 | 61 | return response 62 | except InsufficientScope as e: 63 | response = HttpResponseForbidden() 64 | response["WWW-Authenticate"] = request_error_header(e) 65 | 66 | return response 67 | 68 | return view_func(request, *args, **kwargs) 69 | 70 | return _wrapped_view 71 | 72 | if scopes and hasattr(scopes[0], "__call__"): 73 | func = scopes[0] 74 | scopes = scopes[1:] 75 | return decorator(func) 76 | 77 | return decorator 78 | -------------------------------------------------------------------------------- /doac/exceptions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rediker-Software/doac/398fdd64452e4ff8662297b0381926addd77505a/doac/exceptions/__init__.py -------------------------------------------------------------------------------- /doac/exceptions/access_denied.py: -------------------------------------------------------------------------------- 1 | from .base import AccessDenied 2 | 3 | 4 | class AuthorizationDenied(AccessDenied): 5 | reason = "The request for permission was denied." 6 | -------------------------------------------------------------------------------- /doac/exceptions/base.py: -------------------------------------------------------------------------------- 1 | class AccessDenied(Exception): 2 | error = "access_denied" 3 | 4 | 5 | class InsufficientScope(Exception): 6 | error = "insufficient_scope" 7 | 8 | 9 | class InvalidClient(Exception): 10 | error = "invalid_client" 11 | 12 | 13 | class InvalidGrant(Exception): 14 | error = "invalid_grant" 15 | 16 | 17 | class InvalidRequest(Exception): 18 | error = "invalid_request" 19 | 20 | 21 | class InvalidScope(Exception): 22 | error = "invalid_scope" 23 | 24 | 25 | class InvalidToken(Exception): 26 | error = "invalid_token" 27 | 28 | 29 | class UnsupportedGrantType(Exception): 30 | error = "unsupported_grant_type" 31 | 32 | 33 | class UnsupportedResponseType(Exception): 34 | error = "unsupported_response_type" 35 | -------------------------------------------------------------------------------- /doac/exceptions/insufficient_scope.py: -------------------------------------------------------------------------------- 1 | from .base import InsufficientScope 2 | 3 | 4 | class ScopeNotEnough(InsufficientScope): 5 | reason = "The access token does not have enough scope to access this page." 6 | -------------------------------------------------------------------------------- /doac/exceptions/invalid_client.py: -------------------------------------------------------------------------------- 1 | from .base import InvalidClient 2 | 3 | 4 | class ClientDoesNotExist(InvalidClient): 5 | reason = "The client was malformed or invalid." 6 | 7 | 8 | class ClientSecretNotValid(InvalidClient): 9 | reason = "The client secret was malformed or invalid." 10 | -------------------------------------------------------------------------------- /doac/exceptions/invalid_request.py: -------------------------------------------------------------------------------- 1 | from .base import InvalidRequest 2 | 3 | 4 | class AuthorizationCodeAlreadyUsed(InvalidRequest): 5 | reason = "The authorization code was already used to get a refresh token." 6 | 7 | 8 | class AuthorizationCodeNotProvided(InvalidRequest): 9 | reason = "The authorization code was not provided." 10 | 11 | 12 | class AuthorizationCodeNotValid(InvalidRequest): 13 | reason = "The authorization code was malformed or invalid." 14 | 15 | 16 | class ClientNotProvided(InvalidRequest): 17 | reason = "The client was not provided." 18 | 19 | 20 | class ClientSecretNotProvided(InvalidRequest): 21 | reason = "The client secret was not provided." 22 | 23 | 24 | class CredentialsNotProvided(InvalidRequest): 25 | reason = "No credentials were provided to authenticate the request to view this page." 26 | 27 | 28 | class RedirectUriDoesNotValidate(InvalidRequest): 29 | reason = "The redirect URI does not validate against the client host." 30 | can_redirect = False 31 | 32 | 33 | class RedirectUriNotProvided(InvalidRequest): 34 | reason = "The redirect URI was not provided." 35 | can_redirect = False 36 | 37 | 38 | class RefreshTokenNotProvided(InvalidRequest): 39 | reason = "The refresh token was not provided." 40 | 41 | 42 | class RefreshTokenNotValid(InvalidRequest): 43 | reason = "The refresh token was malformed or invalid." 44 | 45 | 46 | class ResponseTypeNotProvided(InvalidRequest): 47 | reason = "The response type was not provided." 48 | -------------------------------------------------------------------------------- /doac/exceptions/invalid_scope.py: -------------------------------------------------------------------------------- 1 | from .base import InvalidScope 2 | 3 | 4 | class ScopeNotProvided(InvalidScope): 5 | reason = "The scope was malformed or invalid." 6 | 7 | 8 | class ScopeNotValid(InvalidScope): 9 | reason = "The scope contained values which were incorrect." 10 | -------------------------------------------------------------------------------- /doac/exceptions/unsupported_grant_type.py: -------------------------------------------------------------------------------- 1 | from .base import UnsupportedGrantType 2 | 3 | 4 | class GrantTypeNotProvided(UnsupportedGrantType): 5 | reason = "The grant type was malformed or invalid." 6 | 7 | 8 | class GrantTypeNotValid(UnsupportedGrantType): 9 | reason = "The provided grant type is not supported." 10 | -------------------------------------------------------------------------------- /doac/exceptions/unsupported_response_type.py: -------------------------------------------------------------------------------- 1 | from .base import UnsupportedResponseType 2 | 3 | 4 | class ResponseTypeNotValid(UnsupportedResponseType): 5 | reason = "The request type was malformed or invalid." 6 | -------------------------------------------------------------------------------- /doac/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from .models import Client, RedirectUri, Scope 3 | 4 | 5 | class ClientForm(forms.ModelForm): 6 | 7 | class Meta: 8 | fields = ("name", "access_host") 9 | model = Client 10 | 11 | 12 | class RedirectUriForm(forms.ModelForm): 13 | 14 | class Meta: 15 | fields = ("client", "url") 16 | model = RedirectUri 17 | 18 | 19 | class ScopeForm(forms.ModelForm): 20 | 21 | class Meta: 22 | fields = ("short_name", "full_name", "description") 23 | model = Scope 24 | -------------------------------------------------------------------------------- /doac/handlers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rediker-Software/doac/398fdd64452e4ff8662297b0381926addd77505a/doac/handlers/__init__.py -------------------------------------------------------------------------------- /doac/handlers/bearer.py: -------------------------------------------------------------------------------- 1 | from ..exceptions.base import InvalidToken 2 | from ..exceptions.invalid_request import CredentialsNotProvided 3 | from ..models import AccessToken 4 | from ..utils import request_error_header 5 | 6 | 7 | class BearerHandler: 8 | 9 | def access_token(self, value, request): 10 | """ 11 | Try to get the `AccessToken` associated with the provided token. 12 | 13 | *The provided value must pass `BearerHandler.validate()`* 14 | """ 15 | 16 | if self.validate(value, request) is not None: 17 | return None 18 | 19 | access_token = AccessToken.objects.for_token(value) 20 | 21 | return access_token 22 | 23 | def authenticate(self, value, request): 24 | """ 25 | Try to get a user associated with the provided token. 26 | 27 | *The provided value must pass `BearerHandler.validate()`* 28 | """ 29 | 30 | if self.validate(value, request) is not None: 31 | return None 32 | 33 | access_token = AccessToken.objects.for_token(value) 34 | 35 | return access_token.user 36 | 37 | def validate(self, value, request): 38 | """ 39 | Try to get the `AccessToken` associated with the given token. 40 | 41 | The return value is determined based n a few things: 42 | 43 | - If no token is provided (`value` is None), a 400 response will be returned. 44 | - If an invalid token is provided, a 401 response will be returned. 45 | - If the token provided is valid, `None` will be returned. 46 | """ 47 | 48 | from django.http import HttpResponseBadRequest 49 | from doac.http import HttpResponseUnauthorized 50 | 51 | if not value: 52 | response = HttpResponseBadRequest() 53 | response["WWW-Authenticate"] = request_error_header(CredentialsNotProvided) 54 | 55 | return response 56 | 57 | try: 58 | access_token = AccessToken.objects.for_token(value) 59 | except AccessToken.DoesNotExist: 60 | response = HttpResponseUnauthorized() 61 | response["WWW-Authenticate"] = request_error_header(InvalidToken) 62 | 63 | return response 64 | 65 | return None 66 | -------------------------------------------------------------------------------- /doac/http.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | try: 3 | import simplejson as json 4 | except ImportError: 5 | import json 6 | 7 | 8 | class HttpResponseUnauthorized(HttpResponse): 9 | status_code = 401 10 | 11 | 12 | class JsonResponse(HttpResponse): 13 | 14 | def __init__(self, data_dict, *args, **kwargs): 15 | super(JsonResponse, self).__init__(json.dumps(data_dict), mimetype="text/json", *args, **kwargs) 16 | -------------------------------------------------------------------------------- /doac/managers.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.db.models.query import QuerySet 3 | 4 | 5 | class CustomManager(models.Manager): 6 | """ 7 | Custom manager that adds functionality for the custom query set. 8 | """ 9 | 10 | def __getattr__(self, name): 11 | """ 12 | Forwards methods called on the manager to its query set. 13 | """ 14 | 15 | return getattr(self.get_query_set(), name) 16 | 17 | 18 | class AccessTokenManager(CustomManager): 19 | 20 | def get_query_set(self): 21 | return AccessTokenQuerySet(self.model) 22 | 23 | 24 | class AccessTokenQuerySet(QuerySet): 25 | 26 | def for_token(self, token): 27 | return self.get(token=token) 28 | 29 | def is_active(self): 30 | return self.filter(is_active=True) 31 | 32 | def with_client(self, client): 33 | return self.filter(client=client.id) 34 | 35 | def with_refresh_token(self, refresh_token): 36 | return self.filter(refresh_token=refresh_token.id) 37 | 38 | def with_user(self, user): 39 | return self.filter(user=user.pk) 40 | 41 | 42 | class AuthorizationCodeManager(CustomManager): 43 | 44 | def get_query_set(self): 45 | return AuthorizationCodeQuerySet(self.model) 46 | 47 | 48 | class AuthorizationCodeQuerySet(QuerySet): 49 | 50 | def for_token(self, token): 51 | return self.get(token=token) 52 | 53 | def is_active(self): 54 | return self.filter(is_active=True) 55 | 56 | def with_expiration_before(self, date): 57 | return self.filter(expires_at__lt=date) 58 | 59 | def with_client(self, client): 60 | return self.filter(client=client.id) 61 | 62 | def with_user(self, user): 63 | return self.filter(user=user.pk) 64 | 65 | 66 | class AuthorizationTokenManager(CustomManager): 67 | 68 | def get_query_set(self): 69 | return AuthorizationTokenQuerySet(self.model) 70 | 71 | 72 | class AuthorizationTokenQuerySet(QuerySet): 73 | 74 | def for_token(self, token): 75 | return self.get(token=token) 76 | 77 | def is_active(self): 78 | return self.filter(is_active=True) 79 | 80 | def with_client(self, client): 81 | return self.filter(client=client.id) 82 | 83 | def with_user(self, user): 84 | return self.filter(user=user.pk) 85 | 86 | 87 | class ClientManager(CustomManager): 88 | 89 | def get_query_set(self): 90 | return ClientQuerySet(self.model) 91 | 92 | 93 | class ClientQuerySet(QuerySet): 94 | 95 | def for_id(self, id): 96 | return self.get(id=id) 97 | 98 | def for_secret(self, secret): 99 | return self.get(secret=secret) 100 | 101 | def is_active(self): 102 | return self.filter(is_active=True) 103 | 104 | 105 | class RedirectUriManager(CustomManager): 106 | 107 | def get_query_set(self): 108 | return RedirectUriQuerySet(self.model) 109 | 110 | 111 | class RedirectUriQuerySet(QuerySet): 112 | 113 | def for_url(self, url): 114 | return self.get(url=url) 115 | 116 | def with_client(self, client): 117 | return self.filter(client=client.id) 118 | 119 | 120 | class RefreshTokenManager(CustomManager): 121 | 122 | def get_query_set(self): 123 | return RefreshTokenQuerySet(self.model) 124 | 125 | 126 | class RefreshTokenQuerySet(QuerySet): 127 | 128 | def for_token(self, token): 129 | return self.get(token=token) 130 | 131 | def is_active(self): 132 | return self.filter(is_active=True) 133 | 134 | def with_authorization_token(self, authorization_token): 135 | return self.filter(authorization_token=authorization_token.id) 136 | 137 | def with_client(self, client): 138 | return self.filter(client=client.id) 139 | 140 | def with_user(self, user): 141 | return self.filter(user=user.pk) 142 | 143 | 144 | class ScopeManager(CustomManager): 145 | 146 | def get_query_set(self): 147 | return ScopeQuerySet(self.model) 148 | 149 | 150 | class ScopeQuerySet(QuerySet): 151 | 152 | def for_short_name(self, name): 153 | return self.get(short_name=name) 154 | -------------------------------------------------------------------------------- /doac/middleware.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | HANDLERS = ("doac.handlers.bearer.BearerHandler", ) 4 | 5 | 6 | class AuthenticationMiddleware: 7 | 8 | def process_request(self, request): 9 | """ 10 | Try to authenticate the user based on any given tokens that have been provided 11 | to the request object. This will try to detect the authentication type and assign 12 | the detected User object to the `request.user` variable, similar to the standard 13 | Django authentication. 14 | """ 15 | 16 | request.auth_type = None 17 | 18 | http_authorization = request.META.get("HTTP_AUTHORIZATION", None) 19 | 20 | if not http_authorization: 21 | return 22 | 23 | auth = http_authorization.split() 24 | 25 | self.auth_type = auth[0].lower() 26 | self.auth_value = " ".join(auth[1:]).strip() 27 | 28 | request.auth_type = self.auth_type 29 | 30 | self.validate_auth_type() 31 | 32 | if not self.handler_name: 33 | raise Exception("There is no handler defined for this authentication type.") 34 | 35 | self.load_handler() 36 | 37 | response = self.handler.validate(self.auth_value, request) 38 | 39 | if response is not None: 40 | return response 41 | 42 | request.access_token = self.handler.access_token(self.auth_value, request) 43 | request.user = self.handler.authenticate(self.auth_value, request) 44 | 45 | def load_handler(self): 46 | """ 47 | Load the detected handler. 48 | """ 49 | 50 | handler_path = self.handler_name.split(".") 51 | 52 | handler_module = __import__(".".join(handler_path[:-1]), {}, {}, str(handler_path[-1])) 53 | self.handler = getattr(handler_module, handler_path[-1])() 54 | 55 | def validate_auth_type(self): 56 | """ 57 | Validate the detected authorization type against the list of handlers. This will return the full 58 | module path to the detected handler. 59 | """ 60 | 61 | for handler in HANDLERS: 62 | handler_type = handler.split(".")[-2] 63 | 64 | if handler_type == self.auth_type: 65 | self.handler_name = handler 66 | 67 | return 68 | 69 | self.handler_name = None 70 | -------------------------------------------------------------------------------- /doac/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.translation import ugettext_lazy as _ 3 | 4 | from .conf import options 5 | from .compat import get_user_model 6 | from . import managers 7 | 8 | user_model = get_user_model() 9 | 10 | 11 | AUTO_GENERATION_HELP_TEXT = _(u'Leave blank to have it automatically generated.') 12 | 13 | 14 | class AccessToken(models.Model): 15 | user = models.ForeignKey(user_model, related_name="access_tokens") 16 | client = models.ForeignKey("Client", related_name="access_tokens") 17 | 18 | refresh_token = models.ForeignKey("RefreshToken", related_name="access_tokens") 19 | token = models.CharField( 20 | max_length=options.access_token["length"], 21 | blank=True, 22 | help_text=AUTO_GENERATION_HELP_TEXT, 23 | ) 24 | scope = models.ManyToManyField("Scope", related_name="access_tokens") 25 | 26 | created_at = models.DateTimeField(auto_now_add=True) 27 | expires_at = models.DateTimeField() 28 | is_active = models.BooleanField(default=True) 29 | 30 | objects = managers.AccessTokenManager() 31 | 32 | def __unicode__(self): 33 | return self.token 34 | 35 | def generate_token(self): 36 | from .compat import get_random_string 37 | 38 | return get_random_string(options.access_token["length"]) 39 | 40 | def revoke(self): 41 | """ 42 | Revokes an individual access token. This prevents the access token from being used in any future requests. 43 | """ 44 | 45 | self.is_active = False 46 | self.save() 47 | 48 | def save(self, *args, **kwargs): 49 | from .compat import now 50 | 51 | if not self.token: 52 | self.token = self.generate_token() 53 | 54 | if not self.expires_at: 55 | self.expires_at = now() + options.access_token["expires"] 56 | 57 | super(AccessToken, self).save(*args, **kwargs) 58 | 59 | 60 | class AuthorizationCode(models.Model): 61 | client = models.ForeignKey("Client", related_name="authorization_codes") 62 | scope = models.ManyToManyField("Scope", related_name="authorization_codes") 63 | redirect_uri = models.ForeignKey("RedirectUri", related_name="authorization_codes") 64 | 65 | token = models.CharField( 66 | max_length=options.auth_code["length"], 67 | blank=True, 68 | help_text=AUTO_GENERATION_HELP_TEXT, 69 | ) 70 | response_type = models.CharField(choices=(("token", "token"), ("code", "code"), ), max_length=10) 71 | 72 | created_at = models.DateTimeField(auto_now_add=True) 73 | expires_at = models.DateTimeField() 74 | is_active = models.BooleanField(default=True) 75 | 76 | objects = managers.AuthorizationCodeManager() 77 | 78 | def __unicode__(self): 79 | return self.token 80 | 81 | def generate_token(self): 82 | from .compat import get_random_string 83 | 84 | return get_random_string(options.auth_code["length"]) 85 | 86 | def save(self, *args, **kwargs): 87 | from .compat import now 88 | 89 | if not self.token: 90 | self.token = self.generate_token() 91 | 92 | if not self.expires_at: 93 | self.expires_at = now() + options.auth_code["expires"] 94 | 95 | super(AuthorizationCode, self).save(*args, **kwargs) 96 | 97 | 98 | class AuthorizationToken(models.Model): 99 | user = models.ForeignKey(user_model, related_name="authorization_tokens") 100 | client = models.ForeignKey("Client", related_name="authorization_tokens") 101 | token = models.CharField( 102 | max_length=options.auth_token["length"], 103 | blank=True, 104 | help_text=AUTO_GENERATION_HELP_TEXT, 105 | ) 106 | scope = models.ManyToManyField("Scope", related_name="authorization_tokens") 107 | 108 | created_at = models.DateTimeField(auto_now_add=True) 109 | expires_at = models.DateTimeField() 110 | is_active = models.BooleanField(default=True) 111 | 112 | objects = managers.AuthorizationTokenManager() 113 | 114 | def __unicode__(self): 115 | return self.token 116 | 117 | def generate_refresh_token(self): 118 | if self.is_active: 119 | try: 120 | temp = self.refresh_token 121 | 122 | return None 123 | except RefreshToken.DoesNotExist: 124 | self.refresh_token = RefreshToken() 125 | 126 | self.refresh_token.client = self.client 127 | self.refresh_token.user = self.user 128 | self.refresh_token.save() 129 | 130 | self.refresh_token.scope = self.scope.all() 131 | self.refresh_token.save() 132 | 133 | self.is_active = False 134 | self.save() 135 | 136 | return self.refresh_token 137 | 138 | return None 139 | 140 | def generate_token(self): 141 | from .compat import get_random_string 142 | 143 | return get_random_string(options.auth_token["length"]) 144 | 145 | def revoke_tokens(self): 146 | """ 147 | Revoke the authorization token and all tokens that were generated using it. 148 | """ 149 | 150 | self.is_active = False 151 | self.save() 152 | 153 | self.refresh_token.revoke_tokens() 154 | 155 | def save(self, *args, **kwargs): 156 | from .compat import now 157 | 158 | if not self.token: 159 | self.token = self.generate_token() 160 | 161 | if not self.expires_at: 162 | self.expires_at = now() + options.auth_token["expires"] 163 | 164 | super(AuthorizationToken, self).save(*args, **kwargs) 165 | 166 | 167 | class Client(models.Model): 168 | name = models.CharField(max_length=255) 169 | secret = models.CharField( 170 | max_length=options.client["length"], 171 | blank=True, 172 | help_text=AUTO_GENERATION_HELP_TEXT, 173 | ) 174 | access_host = models.URLField(max_length=255) 175 | is_active = models.BooleanField(default=True) 176 | 177 | objects = managers.ClientManager() 178 | 179 | def __unicode__(self): 180 | return self.name 181 | 182 | def generate_secret(self): 183 | from .compat import get_random_string 184 | 185 | return get_random_string(options.client["length"]) 186 | 187 | def save(self, *args, **kwargs): 188 | if not self.secret: 189 | self.secret = self.generate_secret() 190 | 191 | super(Client, self).save(*args, **kwargs) 192 | 193 | 194 | class RedirectUri(models.Model): 195 | client = models.ForeignKey("Client", related_name="redirect_uris") 196 | url = models.URLField(max_length=255) 197 | 198 | objects = managers.RedirectUriManager() 199 | 200 | class Meta: 201 | verbose_name = "redirect URI" 202 | 203 | def __unicode__(self): 204 | return self.url 205 | 206 | 207 | class RefreshToken(models.Model): 208 | user = models.ForeignKey(user_model, related_name="refresh_tokens") 209 | client = models.ForeignKey("Client", related_name="refresh_tokens") 210 | 211 | authorization_token = models.OneToOneField("AuthorizationToken", related_name="refresh_token") 212 | token = models.CharField( 213 | max_length=options.refresh_token["length"], 214 | blank=True, 215 | help_text=AUTO_GENERATION_HELP_TEXT, 216 | ) 217 | scope = models.ManyToManyField("Scope", related_name="refresh_tokens") 218 | 219 | created_at = models.DateTimeField(auto_now_add=True) 220 | expires_at = models.DateTimeField(blank=True, help_text=AUTO_GENERATION_HELP_TEXT) 221 | is_active = models.BooleanField(default=True) 222 | 223 | objects = managers.RefreshTokenManager() 224 | 225 | def __unicode__(self): 226 | return self.token 227 | 228 | def generate_access_token(self): 229 | access_token = AccessToken(client=self.client, user=self.user, refresh_token=self) 230 | access_token.save() 231 | 232 | access_token.scope = self.scope.all() 233 | access_token.save() 234 | 235 | return access_token 236 | 237 | def generate_token(self): 238 | from .compat import get_random_string 239 | 240 | return get_random_string(options.refresh_token["length"]) 241 | 242 | def revoke_tokens(self): 243 | """ 244 | Revokes the refresh token and all access tokens that were generated using it. 245 | """ 246 | 247 | self.is_active = False 248 | self.save() 249 | 250 | for access_token in self.access_tokens.all(): 251 | access_token.revoke() 252 | 253 | def save(self, *args, **kwargs): 254 | from .compat import now 255 | 256 | if not self.token: 257 | self.token = self.generate_token() 258 | 259 | if not self.expires_at: 260 | self.expires_at = now() + options.refresh_token["expires"] 261 | 262 | super(RefreshToken, self).save(*args, **kwargs) 263 | 264 | 265 | class Scope(models.Model): 266 | short_name = models.CharField(max_length=40, unique=True) 267 | full_name = models.CharField(max_length=255) 268 | description = models.TextField() 269 | 270 | objects = managers.ScopeManager() 271 | 272 | def __unicode__(self): 273 | return "%s (%s)" % (self.full_name, self.short_name, ) 274 | -------------------------------------------------------------------------------- /doac/templates/doac/authorize.html: -------------------------------------------------------------------------------- 1 | {% extends "doac/base.html" %} 2 | {% load url from future %} 3 | 4 | {% block oauth_contents %} 5 |
6 | {{ client.name }} is requesting permission to: 7 |
8 | 9 |
    10 | {% for scope in scopes %} 11 |
  • {{ scope.full_name }}
  • 12 | {% endfor %} 13 |
14 | 15 |
16 | {% csrf_token %} 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | {% endblock %} -------------------------------------------------------------------------------- /doac/templates/doac/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ oauth_title }} 5 | 81 | 82 | 83 |
84 |
85 |

{{ oauth_title }}

86 |
87 | {% block oauth_contents %}{% endblock %} 88 |
89 | 90 | -------------------------------------------------------------------------------- /doac/templates/doac/internal_app.html: -------------------------------------------------------------------------------- 1 | {% extends "doac/base.html" %} 2 | 3 | {% block oauth_content %} 4 | 5 | {% endblock %} -------------------------------------------------------------------------------- /doac/urls.py: -------------------------------------------------------------------------------- 1 | try: 2 | from django.conf.urls import patterns, url 3 | except ImportError: 4 | from django.conf.urls.defaults import patterns, url 5 | 6 | from . import views 7 | 8 | urlpatterns = patterns( 9 | '', 10 | url(r"^authorize/$", views.AuthorizeView.as_view(), name="oauth2_authorize"), 11 | url(r"^approval/$", views.ApprovalView.as_view(), name="oauth2_approval"), 12 | url(r"^token/$", views.TokenView.as_view(), name="oauth2_token"), 13 | ) 14 | -------------------------------------------------------------------------------- /doac/utils.py: -------------------------------------------------------------------------------- 1 | def prune_old_authorization_codes(): 2 | """ 3 | Removes all unused and expired authorization codes from the database. 4 | """ 5 | 6 | from .compat import now 7 | from .models import AuthorizationCode 8 | 9 | AuthorizationCode.objects.with_expiration_before(now()).delete() 10 | 11 | 12 | def get_handler(handler_name): 13 | """ 14 | Imports the module for a DOAC handler based on the string representation of the module path that is provided. 15 | """ 16 | 17 | from .conf import options 18 | 19 | handlers = options.handlers 20 | 21 | for handler in handlers: 22 | handler_path = handler.split(".") 23 | name = handler_path[-2] 24 | 25 | if handler_name == name: 26 | handler_module = __import__(".".join(handler_path[:-1]), {}, {}, str(handler_path[-1])) 27 | 28 | return getattr(handler_module, handler_path[-1])() 29 | 30 | return None 31 | 32 | 33 | def request_error_header(exception): 34 | """ 35 | Generates the error header for a request using a Bearer token based on a given OAuth exception. 36 | """ 37 | 38 | from .conf import options 39 | 40 | header = "Bearer realm=\"%s\"" % (options.realm, ) 41 | 42 | if hasattr(exception, "error"): 43 | header = header + ", error=\"%s\"" % (exception.error, ) 44 | 45 | if hasattr(exception, "reason"): 46 | header = header + ", error_description=\"%s\"" % (exception.reason, ) 47 | 48 | return header 49 | 50 | 51 | def total_seconds(delta): 52 | """ 53 | Get the total seconds that a `datetime.timedelta` object covers. Used for returning the total 54 | time until a token expires during the handshake process. 55 | """ 56 | 57 | return delta.days * 86400 + delta.seconds 58 | -------------------------------------------------------------------------------- /doac/views.py: -------------------------------------------------------------------------------- 1 | from django.template.response import TemplateResponse 2 | from django.views.decorators.csrf import csrf_exempt 3 | from django.views.generic import View 4 | 5 | from .utils import total_seconds 6 | from . import utils 7 | 8 | 9 | ALLOWED_RESPONSE_TYPES = ("code", "token", ) 10 | 11 | ALLOWED_GRANT_TYPES = ("authorization_code", "refresh_token", ) 12 | 13 | 14 | class OAuthView(View): 15 | """ 16 | All views must subclass this class. 17 | 18 | This provides common methods which are needed for validation and processing OAuth 19 | requests and responses. 20 | """ 21 | 22 | def handle_exception(self, exception): 23 | """ 24 | Handle a unspecified exception and return the correct method that should be used 25 | for handling it. 26 | 27 | If the exception has the `can_redirect` property set to False, it is 28 | rendered to the browser. Otherwise, it will be redirected to the location 29 | provided in the `RedirectUri` object that is associated with the request. 30 | """ 31 | 32 | can_redirect = getattr(exception, "can_redirect", True) 33 | redirect_uri = getattr(self, "redirect_uri", None) 34 | 35 | if can_redirect and redirect_uri: 36 | return self.redirect_exception(exception) 37 | else: 38 | return self.render_exception(exception) 39 | 40 | def redirect_exception(self, exception): 41 | """ 42 | Build the query string for the exception and return a redirect to the 43 | redirect uri that was associated with the request. 44 | """ 45 | 46 | from django.http import QueryDict, HttpResponseRedirect 47 | 48 | query = QueryDict("").copy() 49 | query["error"] = exception.error 50 | query["error_description"] = exception.reason 51 | query["state"] = self.state 52 | 53 | return HttpResponseRedirect(self.redirect_uri.url + "?" + query.urlencode()) 54 | 55 | def render_exception(self, exception): 56 | """ 57 | Return a 401 response with the body being the reason for the exception. 58 | """ 59 | 60 | from .http import HttpResponseUnauthorized 61 | 62 | return HttpResponseUnauthorized(exception.reason) 63 | 64 | def render_exception_js(self, exception): 65 | """ 66 | Return a response with the body containing a JSON-formatter version of the exception. 67 | """ 68 | 69 | from .http import JsonResponse 70 | 71 | response = {} 72 | response["error"] = exception.error 73 | response["error_description"] = exception.reason 74 | 75 | return JsonResponse(response, status=getattr(exception, 'code', 400)) 76 | 77 | def verify_dictionary(self, dict, *args): 78 | """ 79 | Based on a provided `dict`, validate all of the contents of that dictionary that are 80 | provided. 81 | 82 | For each argument provided that isn't the dictionary, this will set the raw value of 83 | that key as the instance variable of the same name. It will then call the verification 84 | function named `verify_[argument]` to verify the data. 85 | """ 86 | 87 | for arg in args: 88 | setattr(self, arg, dict.get(arg, None)) 89 | 90 | if hasattr(self, "verify_" + arg): 91 | func = getattr(self, "verify_" + arg) 92 | func() 93 | 94 | def verify_client_id(self): 95 | """ 96 | Verify a provided client id against the database and set the `Client` object that is 97 | associated with it to `self.client`. 98 | 99 | TODO: Document all of the thrown exceptions. 100 | """ 101 | 102 | from .models import Client 103 | from .exceptions.invalid_client import ClientDoesNotExist 104 | from .exceptions.invalid_request import ClientNotProvided 105 | 106 | if self.client_id: 107 | try: 108 | self.client = Client.objects.for_id(self.client_id) 109 | # Catching also ValueError for the case when client_id doesn't contain an integer. 110 | except (Client.DoesNotExist, ValueError): 111 | raise ClientDoesNotExist() 112 | else: 113 | raise ClientNotProvided() 114 | 115 | def verify_redirect_uri(self): 116 | from urlparse import urlparse 117 | from .models import RedirectUri 118 | from .exceptions.invalid_request import RedirectUriDoesNotValidate, RedirectUriNotProvided 119 | 120 | PARSE_MATCH_ATTRIBUTES = ("scheme", "hostname", "port", ) 121 | 122 | if self.redirect_uri: 123 | client_host = self.client.access_host 124 | client_parse = urlparse(client_host) 125 | 126 | redirect_parse = urlparse(self.redirect_uri) 127 | 128 | for attribute in PARSE_MATCH_ATTRIBUTES: 129 | client_attribute = getattr(client_parse, attribute) 130 | redirect_attribute = getattr(redirect_parse, attribute) 131 | 132 | if not client_attribute == redirect_attribute: 133 | raise RedirectUriDoesNotValidate() 134 | 135 | try: 136 | self.redirect_uri = RedirectUri.objects.with_client(self.client).for_url(self.redirect_uri) 137 | except RedirectUri.DoesNotExist: 138 | raise RedirectUriDoesNotValidate() 139 | else: 140 | raise RedirectUriNotProvided() 141 | 142 | 143 | class ApprovalView(OAuthView): 144 | 145 | http_method_names = ("post", ) 146 | 147 | def post(self, request, *args, **kwargs): 148 | utils.prune_old_authorization_codes() 149 | 150 | try: 151 | self.verify_dictionary(request.POST, "code") 152 | except Exception, e: 153 | return self.render_exception(e) 154 | 155 | self.client = self.authorization_code.client 156 | self.redirect_uri = self.authorization_code.redirect_uri 157 | self.scopes = self.authorization_code.scope.all() 158 | self.state = request.POST.get("code_state", None) 159 | 160 | if "deny_access" in request.POST: 161 | return self.authorization_denied() 162 | else: 163 | return self.authorization_accepted() 164 | 165 | def authorization_accepted(self): 166 | from django.http import HttpResponseRedirect 167 | from .models import AuthorizationToken 168 | 169 | self.authorization_token = AuthorizationToken(user=self.request.user, client=self.client) 170 | self.authorization_token.save() 171 | 172 | self.authorization_token.scope = self.scopes 173 | self.authorization_token.save() 174 | 175 | if self.authorization_code.response_type == "code": 176 | separator = "?" 177 | else: 178 | separator = "#" 179 | 180 | self.access_token = self.authorization_token.generate_refresh_token().generate_access_token() 181 | 182 | query_string = self.generate_query_string() 183 | 184 | return HttpResponseRedirect(self.redirect_uri.url + separator + query_string) 185 | 186 | def authorization_denied(self): 187 | from .exceptions.access_denied import AuthorizationDenied 188 | 189 | return self.redirect_exception(AuthorizationDenied()) 190 | 191 | def generate_query_string(self): 192 | from django.http import QueryDict 193 | 194 | query = QueryDict("").copy() 195 | query["state"] = self.state 196 | 197 | if self.authorization_code.response_type == "token": 198 | query["access_token"] = self.access_token.token 199 | else: 200 | query["code"] = self.authorization_token.token 201 | 202 | return query.urlencode() 203 | 204 | def verify_code(self): 205 | from .models import AuthorizationCode 206 | from .exceptions.invalid_request import AuthorizationCodeNotValid, AuthorizationCodeNotProvided 207 | 208 | if self.code: 209 | get_code = self.request.GET.get("code", None) 210 | 211 | if not get_code == self.code: 212 | raise AuthorizationCodeNotValid() 213 | 214 | try: 215 | self.authorization_code = AuthorizationCode.objects.for_token(self.code) 216 | except AuthorizationCode.DoesNotExist: 217 | raise AuthorizationCodeNotValid() 218 | else: 219 | raise AuthorizationCodeNotProvided() 220 | 221 | 222 | class AuthorizeView(OAuthView): 223 | 224 | http_method_names = ("get", "post", ) 225 | 226 | def get(self, request, *args, **kwargs): 227 | from django.contrib.auth.views import redirect_to_login 228 | 229 | utils.prune_old_authorization_codes() 230 | 231 | self.state = request.GET.get("state", "o2cs") 232 | 233 | try: 234 | self.verify_dictionary(request.GET, "client_id", "redirect_uri", "scope", "response_type") 235 | except Exception, e: 236 | return self.handle_exception(e) 237 | 238 | if not request.user.is_active: 239 | return redirect_to_login(request.get_full_path()) 240 | 241 | code = self.generate_authorization_code() 242 | 243 | context = { 244 | "authorization_code": code, 245 | "client": self.client, 246 | "oauth_title": "Request for Permission", 247 | "scopes": self.scopes, 248 | "state": self.state, 249 | } 250 | 251 | return TemplateResponse(request, "doac/authorize.html", context) 252 | 253 | def generate_authorization_code(self): 254 | from .models import AuthorizationCode 255 | 256 | code = AuthorizationCode(client=self.client, redirect_uri=self.redirect_uri, response_type=self.response_type) 257 | code.save() 258 | 259 | code.scope = self.scopes 260 | code.save() 261 | 262 | return code 263 | 264 | def verify_response_type(self): 265 | from .exceptions.unsupported_response_type import ResponseTypeNotValid 266 | from .exceptions.invalid_request import ResponseTypeNotProvided 267 | 268 | if self.response_type: 269 | if not self.response_type in ALLOWED_RESPONSE_TYPES: 270 | raise ResponseTypeNotValid() 271 | else: 272 | raise ResponseTypeNotProvided() 273 | 274 | def verify_scope(self): 275 | from .models import Scope 276 | from .exceptions.invalid_scope import ScopeNotProvided, ScopeNotValid 277 | 278 | if self.scope: 279 | scopes = self.scope.split(" ") 280 | self.scopes = [] 281 | 282 | for scope_name in scopes: 283 | try: 284 | scope = Scope.objects.for_short_name(scope_name) 285 | except Scope.DoesNotExist: 286 | raise ScopeNotValid() 287 | 288 | self.scopes.append(scope) 289 | else: 290 | raise ScopeNotProvided() 291 | 292 | 293 | class TokenView(OAuthView): 294 | 295 | http_method_names = ("post", ) 296 | 297 | @csrf_exempt 298 | def dispatch(self, *args, **kwargs): 299 | return super(TokenView, self).dispatch(*args, **kwargs) 300 | 301 | def post(self, request, *args, **kwargs): 302 | try: 303 | self.verify_dictionary(request.POST, "grant_type", "client_id", "client_secret") 304 | except Exception, e: 305 | return self.render_exception_js(e) 306 | 307 | if self.grant_type == "authorization_code": 308 | try: 309 | self.verify_dictionary(request.POST, "code") 310 | except Exception, e: 311 | return self.render_exception_js(e) 312 | 313 | self.refresh_token = self.authorization_token.generate_refresh_token() 314 | 315 | if not self.refresh_token: 316 | self.authorization_token.revoke_tokens() 317 | 318 | self.access_token = self.refresh_token.generate_access_token() 319 | 320 | return self.render_authorization_token() 321 | 322 | elif self.grant_type == "refresh_token": 323 | try: 324 | self.verify_dictionary(request.POST, "refresh_token") 325 | except Exception, e: 326 | return self.render_exception_js(e) 327 | 328 | self.access_token = self.refresh_token.generate_access_token() 329 | 330 | return self.render_refresh_token() 331 | 332 | def render_authorization_token(self): 333 | from .compat import now 334 | from .http import JsonResponse 335 | 336 | remaining = self.refresh_token.expires_at - now() 337 | 338 | response = {} 339 | response["refresh_token"] = self.refresh_token.token 340 | response["token_type"] = "bearer" 341 | response["expires_in"] = int(total_seconds(remaining)) 342 | response["access_token"] = self.access_token.token 343 | 344 | return JsonResponse(response) 345 | 346 | def render_refresh_token(self): 347 | from .compat import now 348 | from .http import JsonResponse 349 | 350 | remaining = self.access_token.expires_at - now() 351 | 352 | response = {} 353 | response["token_type"] = "bearer" 354 | response["expires_in"] = int(total_seconds(remaining)) 355 | response["access_token"] = self.access_token.token 356 | 357 | return JsonResponse(response) 358 | 359 | def verify_client_secret(self): 360 | from .exceptions.invalid_client import ClientSecretNotValid 361 | from .exceptions.invalid_request import ClientSecretNotProvided 362 | 363 | if self.client_secret: 364 | if not self.client.secret == self.client_secret: 365 | raise ClientSecretNotValid() 366 | else: 367 | raise ClientSecretNotProvided() 368 | 369 | def verify_code(self): 370 | from .exceptions.invalid_request import AuthorizationCodeAlreadyUsed, AuthorizationCodeNotProvided, AuthorizationCodeNotValid 371 | from .models import AuthorizationToken 372 | 373 | if self.code: 374 | try: 375 | self.authorization_token = AuthorizationToken.objects.with_client(self.client).for_token(self.code) 376 | 377 | if not self.authorization_token.is_active: 378 | self.authorization_token.revoke_tokens() 379 | 380 | raise AuthorizationCodeAlreadyUsed() 381 | except AuthorizationToken.DoesNotExist: 382 | raise AuthorizationCodeNotValid() 383 | else: 384 | raise AuthorizationCodeNotProvided() 385 | 386 | def verify_grant_type(self): 387 | from .exceptions.unsupported_grant_type import GrantTypeNotProvided, GrantTypeNotValid 388 | 389 | self.grant_type = self.request.POST.get("grant_type", None) 390 | 391 | if self.grant_type: 392 | if not self.grant_type in ALLOWED_GRANT_TYPES: 393 | raise GrantTypeNotValid() 394 | else: 395 | raise GrantTypeNotProvided() 396 | 397 | def verify_refresh_token(self): 398 | from .exceptions.invalid_request import RefreshTokenNotProvided, RefreshTokenNotValid 399 | from .models import RefreshToken 400 | 401 | if self.refresh_token: 402 | try: 403 | self.refresh_token = RefreshToken.objects.with_client(self.client).for_token(self.refresh_token) 404 | except RefreshToken.DoesNotExist: 405 | raise RefreshTokenNotValid() 406 | else: 407 | raise RefreshTokenNotProvided() 408 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | DOAC comes prepared with all of the things you need to quickly get an OAuth 2.0 solution for your project. 5 | 6 | * [Exceptions](exceptions/index.md) 7 | * [Models](models/index.md) 8 | * [Utilities](utilities.md) 9 | * [Views](views/index.md) 10 | -------------------------------------------------------------------------------- /docs/exceptions/base.md: -------------------------------------------------------------------------------- 1 | =============== 2 | Base Exceptions 3 | =============== 4 | 5 | ### *exception* oauth2_consumer.exceptions.base.AccessDenied ### 6 | 7 | The `AccessDenied` exception is raised during the approval step of the authorization process if the user rejects the clients request for permission. The OAuth ``error`` for this exception is ``access_denied``. 8 | 9 | ### *exception* oauth2_consumer.exceptions.base.InvalidClient ### 10 | 11 | The `InvalidClient` exception is raised if a client was provided but had an error. 12 | 13 | ### *exception* oauth2_consumer.exceptions.base.InvalidGrant ### 14 | 15 | ### *exception* oauth2_consumer.exceptions.base.InvalidRequest ### 16 | 17 | The `InvalidRequest` exception is raised because a parameter did not pass validation or was not provided. The OAuth ``error`` for this exception is ``invalid_request``. 18 | 19 | This can be raised during the initial authorization request because: 20 | 21 | - A required parameter was not provideed. 22 | - A supplied parameter failed its verification check. 23 | 24 | This exception is not intended to be redirected to the client during the authorization stage. 25 | 26 | ### *exception* oauth2_consumer.exceptions.base.InvalidScope ### 27 | 28 | The `InvalidScope` exception is raised because the scope that was provided for the request does not pass validation or was not provided. The OAuth ``error`` for this exception is ``invalid_scope``. 29 | 30 | ### *exception* oauth2_consumer.exceptions.base.UnsupportedGrantType ### 31 | 32 | The `UnsupportedGrantType` exception is raised during the exchanging of tokens if the specified grant type is in the list of suppported grant types, or was not provided. 33 | 34 | ### *exception* oauth2_consumer.exceptions.base.UnsupportedResponseType ### 35 | 36 | The `UnsupportedResponseType` exception is raised during the initial authorization step because the requested ``response_type`` was not supported. The OAuth ``error`` for this exception is ``unsupported_response_type``. 37 | -------------------------------------------------------------------------------- /docs/exceptions/index.md: -------------------------------------------------------------------------------- 1 | ========== 2 | Exceptions 3 | ========== 4 | 5 | Django OAuth2 Consumer raises multiple exceptions when authorizing users under clients in order to control what errors are returned. 6 | 7 | Contents: 8 | 9 | * [Base Exceptions](base.md) 10 | * [Invalid Client Exceptions](invalid_client.md) 11 | * [Invalid Request Exceptions](invalid_request.md) 12 | * [Invalid Scope Exceptions](invalid_scope.md) 13 | * [Unsupported Grant Type Exceptions](unsupported.md) 14 | -------------------------------------------------------------------------------- /docs/exceptions/invalid_client.md: -------------------------------------------------------------------------------- 1 | ========================= 2 | Invalid Client Exceptions 3 | ========================= 4 | 5 | ### *exception* oauth2_consumer.exceptions.invalid_client.ClientDoesNotExist ### 6 | 7 | ### *exception* oauth2_consumer.exceptions.invalid_client.ClientSecretNotValid ### 8 | -------------------------------------------------------------------------------- /docs/exceptions/invalid_request.md: -------------------------------------------------------------------------------- 1 | ========================== 2 | Invalid Request Exceptions 3 | ========================== 4 | 5 | ### *exception* oauth2_consumer.exceptions.invalid_request.AuthorizationCodeAlreadyUsed ### 6 | 7 | ### *exception* oauth2_consumer.exceptions.invalid_request.AuthorizationCodeNotProvided ### 8 | 9 | ### *exception* oauth2_consumer.exceptions.invalid_request.AuthorizationCodeNotValid ### 10 | 11 | ### *exception* oauth2_consumer.exceptions.invalid_request.ClientNotProvided ### 12 | 13 | ### *exception* oauth2_consumer.exceptions.invalid_request.ClientSecretNotProvided ### 14 | 15 | ### *exception* oauth2_consumer.exceptions.invalid_request.RedirectUriNotProvided ### 16 | 17 | ### *exception* oauth2_consumer.exceptions.invalid_request.RedirectUriDoesNotValidate ### 18 | 19 | ### *exception* oauth2_consumer.exceptions.invalid_request.ResponseTypeNotProvided ### -------------------------------------------------------------------------------- /docs/exceptions/invalid_scope.md: -------------------------------------------------------------------------------- 1 | ======================== 2 | Invalid Scope Exceptions 3 | ======================== 4 | 5 | ### *exception* oauth2_consumer.exceptions.invalid_scope.ScopeNotProvided ### 6 | 7 | ### *exception* oauth2_consumer.exceptions.invalid_scope.ScopeNotValid ### -------------------------------------------------------------------------------- /docs/exceptions/unsupported.md: -------------------------------------------------------------------------------- 1 | ================================= 2 | Unsupported Grant Type Exceptions 3 | ================================= 4 | 5 | ### *exception* oauth2_consumer.exceptions.unsupported_grant_type.GrantTypeNotProvided ### 6 | 7 | ### *exception* oauth2_consumer.exceptions.unsupported_grant_type.GrantTypeNotValid ### -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | Welcome to Django OAuth2 Consumer's documentation! 2 | ================================================== 3 | 4 | Django OAuth2 Consumer (DOAC) is a reusable application that can be used to provide an OAuth consumer for your project. 5 | 6 | * [Installation](installation.md) 7 | * [API](api.md) 8 | * [Exceptions](exceptions/index.md) 9 | * [Models](models/index.md) 10 | * [Utilities](utilities.md) 11 | * [Views](views/index.md) 12 | * [Settings](settings.md) 13 | * [Integrations](integrations.md) 14 | 15 | Requirements 16 | ============ 17 | 18 | We tried to make it so that this application did not require anything, but that is pretty illogical when you think about it, so we settled with a short list of requirements that should fit your project anyway. This application may work on different setups, but we probably haven't tested them, so contact us if you find that there is an issue with our list of requirements. 19 | 20 | Required 21 | -------- 22 | 23 | - Django 1.3+ 24 | - Django [authentication application](https://docs.djangoproject.com/en/1.5/topics/auth/) 25 | - Python 2.6+ 26 | 27 | This application is directly compatible with other tools and applications, but if you aren't using them it shouldn't make a difference. We provide extra functionality by default if it makes sense to do so. 28 | 29 | Optional 30 | -------- 31 | 32 | - Django [admin application](https://docs.djangoproject.com/en/1.5/ref/contrib/admin/) 33 | 34 | Getting Help 35 | ============ 36 | 37 | If you find a bug, have an idea for a feature, or just need some guidance, we provide support through our GitHub repository. Just open up a new issue and make sure to include as much information as possible so we can try our best to determine the problem. A working test case or example is always preferred, though we recognize that it is not always possible to provide one. 38 | 39 | The issue tracker is available here: 40 | 41 | Contributing 42 | ============ 43 | 44 | Django OAuth2 Consumer is an open-source application which you can contribute to. We will provide instructions for those interested in the future. 45 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Django OAuth2 Consumer (DOAC) is on PyPi! 5 | 6 | Using Pip 7 | --------- 8 | 9 | > pip install doac 10 | 11 | Then all you need to do is add `doac` to your `INSTALLED_APPS`. 12 | 13 | ``` 14 | INSTALLED_APPS = ( 15 | ... 16 | "doac", 17 | ) 18 | ``` 19 | 20 | And set up the tables for everything. 21 | 22 | ``` 23 | python manage.py syncdb 24 | ``` 25 | 26 | Doing it manually 27 | ----------------- 28 | 29 | You can still manually install DOAC, but it is recommended to install it using `pip`. 30 | 31 | ### 1. Copy DOAC Files 32 | 33 | Copy the files from GitHub into your project directory to a folder called `doac`. 34 | 35 | ### 2. Add DOAC to your settings 36 | 37 | Add `doac` to your `INSTALLED_APPS`. 38 | 39 | ### 3. Set up the database 40 | 41 | Run `python manage.py syncdb` to install the tables for DOAC. 42 | -------------------------------------------------------------------------------- /docs/integrations.md: -------------------------------------------------------------------------------- 1 | Integrating DOAC with other applications 2 | ======================================== 3 | DOAC should be compatible with any application that requires the default Django authentication. But in some cases this is too broad, or you just need finer control over how it all works. 4 | 5 | Django Rest Framework 6 | ---------------------- 7 | DOAC supports both authentication and permissions checking through Django Rest Framework. This allows you to restrict authentication through access tokens to just the parts of your site which require it. 8 | 9 | ### Requirements 10 | 11 | In order to use DOAC with Django Rest Framework, you must install them both first. 12 | 13 | > pip install doac djangorestframework 14 | 15 | ### Integrating the authentication 16 | 17 | You can use the authentication on a per-view basis or on a global level, DOAC works fine wherever you define the authentication for your API. 18 | 19 | Globally, through the settings: 20 | ``` 21 | REST_FRAMEWORK = { 22 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 23 | 'doac.contrib.rest_framework.authentication.DoacAuthentication', 24 | ), 25 | } 26 | ``` 27 | 28 | Locally, using the API: 29 | ``` 30 | from rest_framework import viewsets 31 | from doac.contrib.rest_framework import authentication 32 | 33 | class ExampleViewSet(viewsets.ModelViewSet): 34 | authentication_classes = [authentication.DoacAuthentication] 35 | model = ExampleModel 36 | ``` 37 | 38 | ### Integrating the permissions 39 | 40 | OAuth2 uses scopes to define what an access token can do with an application. DOAC allows you to specify what scopes are allowed for accessing a viewset. 41 | 42 | ``` 43 | from rest_framework import viewsets 44 | from doac.contrib.rest_framework import authentication, permissions 45 | 46 | class ExampleViewSet(viewsets.ModelViewSet): 47 | authentication_classes = [authentication.DoacAuthentication] 48 | permissions_classes = [permissions.TokenHasScope] 49 | model = ExampleModel 50 | 51 | scopes = ["read", "write", "fun_stuff"] 52 | ``` 53 | 54 | The scopes are checked in the same way as the `scope_required` decorator. If no scopes are specified, all access tokens which have a scope are allowed access. Any and all scopes specified will be checked in order to access the view, and any missing scopes will result in the access token being denied. 55 | 56 | Insert application name here 57 | ---------------------------- 58 | Do you have an application that DOAC integrates with? We are accepting [pull requests](https://github.com/Rediker-Software/doac) to the documentation, which means you can add your information here. -------------------------------------------------------------------------------- /docs/markdown/api.md: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | DOAC comes prepared with all of the things you need to quickly get an OAuth 2.0 solution for your project. 5 | 6 | * [Exceptions](exceptions/index.md) 7 | * [Models](models/index.md) 8 | * [Utilities](utilities.md) 9 | * [Views](views/index.md) 10 | -------------------------------------------------------------------------------- /docs/markdown/exceptions/base.md: -------------------------------------------------------------------------------- 1 | =============== 2 | Base Exceptions 3 | =============== 4 | 5 | ### *exception* oauth2_consumer.exceptions.base.AccessDenied ### 6 | 7 | The `AccessDenied` exception is raised during the approval step of the authorization process if the user rejects the clients request for permission. The OAuth ``error`` for this exception is ``access_denied``. 8 | 9 | ### *exception* oauth2_consumer.exceptions.base.InvalidClient ### 10 | 11 | The `InvalidClient` exception is raised if a client was provided but had an error. 12 | 13 | ### *exception* oauth2_consumer.exceptions.base.InvalidGrant ### 14 | 15 | ### *exception* oauth2_consumer.exceptions.base.InvalidRequest ### 16 | 17 | The `InvalidRequest` exception is raised because a parameter did not pass validation or was not provided. The OAuth ``error`` for this exception is ``invalid_request``. 18 | 19 | This can be raised during the initial authorization request because: 20 | 21 | - A required parameter was not provideed. 22 | - A supplied parameter failed its verification check. 23 | 24 | This exception is not intended to be redirected to the client during the authorization stage. 25 | 26 | ### *exception* oauth2_consumer.exceptions.base.InvalidScope ### 27 | 28 | The `InvalidScope` exception is raised because the scope that was provided for the request does not pass validation or was not provided. The OAuth ``error`` for this exception is ``invalid_scope``. 29 | 30 | ### *exception* oauth2_consumer.exceptions.base.UnsupportedGrantType ### 31 | 32 | The `UnsupportedGrantType` exception is raised during the exchanging of tokens if the specified grant type is in the list of suppported grant types, or was not provided. 33 | 34 | ### *exception* oauth2_consumer.exceptions.base.UnsupportedResponseType ### 35 | 36 | The `UnsupportedResponseType` exception is raised during the initial authorization step because the requested ``response_type`` was not supported. The OAuth ``error`` for this exception is ``unsupported_response_type``. 37 | -------------------------------------------------------------------------------- /docs/markdown/exceptions/index.md: -------------------------------------------------------------------------------- 1 | ========== 2 | Exceptions 3 | ========== 4 | 5 | Django OAuth2 Consumer raises multiple exceptions when authorizing users under clients in order to control what errors are returned. 6 | 7 | Contents: 8 | 9 | * [Base Exceptions](base.md) 10 | * [Invalid Client Exceptions](invalid_client.md) 11 | * [Invalid Request Exceptions](invalid_request.md) 12 | * [Invalid Scope Exceptions](invalid_scope.md) 13 | * [Unsupported Grant Type Exceptions](unsupported.md) 14 | -------------------------------------------------------------------------------- /docs/markdown/exceptions/invalid_client.md: -------------------------------------------------------------------------------- 1 | ========================= 2 | Invalid Client Exceptions 3 | ========================= 4 | 5 | ### *exception* oauth2_consumer.exceptions.invalid_client.ClientDoesNotExist ### 6 | 7 | ### *exception* oauth2_consumer.exceptions.invalid_client.ClientSecretNotValid ### 8 | -------------------------------------------------------------------------------- /docs/markdown/exceptions/invalid_request.md: -------------------------------------------------------------------------------- 1 | ========================== 2 | Invalid Request Exceptions 3 | ========================== 4 | 5 | ### *exception* oauth2_consumer.exceptions.invalid_request.AuthorizationCodeAlreadyUsed ### 6 | 7 | ### *exception* oauth2_consumer.exceptions.invalid_request.AuthorizationCodeNotProvided ### 8 | 9 | ### *exception* oauth2_consumer.exceptions.invalid_request.AuthorizationCodeNotValid ### 10 | 11 | ### *exception* oauth2_consumer.exceptions.invalid_request.ClientNotProvided ### 12 | 13 | ### *exception* oauth2_consumer.exceptions.invalid_request.ClientSecretNotProvided ### 14 | 15 | ### *exception* oauth2_consumer.exceptions.invalid_request.RedirectUriNotProvided ### 16 | 17 | ### *exception* oauth2_consumer.exceptions.invalid_request.RedirectUriDoesNotValidate ### 18 | 19 | ### *exception* oauth2_consumer.exceptions.invalid_request.ResponseTypeNotProvided ### -------------------------------------------------------------------------------- /docs/markdown/exceptions/invalid_scope.md: -------------------------------------------------------------------------------- 1 | ======================== 2 | Invalid Scope Exceptions 3 | ======================== 4 | 5 | ### *exception* oauth2_consumer.exceptions.invalid_scope.ScopeNotProvided ### 6 | 7 | ### *exception* oauth2_consumer.exceptions.invalid_scope.ScopeNotValid ### -------------------------------------------------------------------------------- /docs/markdown/exceptions/unsupported.md: -------------------------------------------------------------------------------- 1 | ================================= 2 | Unsupported Grant Type Exceptions 3 | ================================= 4 | 5 | ### *exception* oauth2_consumer.exceptions.unsupported_grant_type.GrantTypeNotProvided ### 6 | 7 | ### *exception* oauth2_consumer.exceptions.unsupported_grant_type.GrantTypeNotValid ### -------------------------------------------------------------------------------- /docs/markdown/index.md: -------------------------------------------------------------------------------- 1 | Welcome to Django OAuth2 Consumer's documentation! 2 | ================================================== 3 | 4 | Django OAuth2 Consumer (DOAC) is a reusable application that can be used to provide an OAuth consumer for your project. 5 | 6 | * [Installation](installation.md) 7 | * [API](api.md) 8 | * [Exceptions](exceptions/index.md) 9 | * [Models](models/index.md) 10 | * [Utilities](utilities.md) 11 | * [Views](views/index.md) 12 | * [Settings](settings.md) 13 | * [Integrations](integrations.md) 14 | 15 | Requirements 16 | ============ 17 | 18 | We tried to make it so that this application did not require anything, but that is pretty illogical when you think about it, so we settled with a short list of requirements that should fit your project anyway. This application may work on different setups, but we probably haven't tested them, so contact us if you find that there is an issue with our list of requirements. 19 | 20 | Required 21 | -------- 22 | 23 | - Django 1.3+ 24 | - Django [authentication application](https://docs.djangoproject.com/en/1.5/topics/auth/) 25 | - Python 2.6+ 26 | 27 | This application is directly compatible with other tools and applications, but if you aren't using them it shouldn't make a difference. We provide extra functionality by default if it makes sense to do so. 28 | 29 | Optional 30 | -------- 31 | 32 | - Django [admin application](https://docs.djangoproject.com/en/1.5/ref/contrib/admin/) 33 | 34 | Getting Help 35 | ============ 36 | 37 | If you find a bug, have an idea for a feature, or just need some guidance, we provide support through our GitHub repository. Just open up a new issue and make sure to include as much information as possible so we can try our best to determine the problem. A working test case or example is always preferred, though we recognize that it is not always possible to provide one. 38 | 39 | The issue tracker is available here: 40 | 41 | Contributing 42 | ============ 43 | 44 | Django OAuth2 Consumer is an open-source application which you can contribute to. We will provide instructions for those interested in the future. 45 | -------------------------------------------------------------------------------- /docs/markdown/installation.md: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Django OAuth2 Consumer (DOAC) is on PyPi! 5 | 6 | Using Pip 7 | --------- 8 | 9 | > pip install doac 10 | 11 | Then all you need to do is add `doac` to your `INSTALLED_APPS`. 12 | 13 | ``` 14 | INSTALLED_APPS = ( 15 | ... 16 | "doac", 17 | ) 18 | ``` 19 | 20 | And set up the tables for everything. 21 | 22 | ``` 23 | python manage.py syncdb 24 | ``` 25 | 26 | Doing it manually 27 | ----------------- 28 | 29 | You can still manually install DOAC, but it is recommended to install it using `pip`. 30 | 31 | ### 1. Copy DOAC Files 32 | 33 | Copy the files from GitHub into your project directory to a folder called `doac`. 34 | 35 | ### 2. Add DOAC to your settings 36 | 37 | Add `doac` to your `INSTALLED_APPS`. 38 | 39 | ### 3. Set up the database 40 | 41 | Run `python manage.py syncdb` to install the tables for DOAC. 42 | -------------------------------------------------------------------------------- /docs/markdown/integrations.md: -------------------------------------------------------------------------------- 1 | Integrating DOAC with other applications 2 | ======================================== 3 | DOAC should be compatible with any application that requires the default Django authentication. But in some cases this is too broad, or you just need finer control over how it all works. 4 | 5 | Django Rest Framework 6 | ---------------------- 7 | DOAC supports both authentication and permissions checking through Django Rest Framework. This allows you to restrict authentication through access tokens to just the parts of your site which require it. 8 | 9 | ### Requirements 10 | 11 | In order to use DOAC with Django Rest Framework, you must install them both first. 12 | 13 | > pip install doac djangorestframework 14 | 15 | ### Integrating the authentication 16 | 17 | You can use the authentication on a per-view basis or on a global level, DOAC works fine wherever you define the authentication for your API. 18 | 19 | Globally, through the settings: 20 | ``` 21 | REST_FRAMEWORK = { 22 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 23 | 'doac.contrib.rest_framework.authentication.DoacAuthentication', 24 | ), 25 | } 26 | ``` 27 | 28 | Locally, using the API: 29 | ``` 30 | from rest_framework import viewsets 31 | from doac.contrib.rest_framework import authentication 32 | 33 | class ExampleViewSet(viewsets.ModelViewSet): 34 | authentication_classes = [authentication.DoacAuthentication] 35 | model = ExampleModel 36 | ``` 37 | 38 | ### Integrating the permissions 39 | 40 | OAuth2 uses scopes to define what an access token can do with an application. DOAC allows you to specify what scopes are allowed for accessing a viewset. 41 | 42 | ``` 43 | from rest_framework import viewsets 44 | from doac.contrib.rest_framework import authentication, permissions 45 | 46 | class ExampleViewSet(viewsets.ModelViewSet): 47 | authentication_classes = [authentication.DoacAuthentication] 48 | permissions_classes = [permissions.TokenHasScope] 49 | model = ExampleModel 50 | 51 | scopes = ["read", "write", "fun_stuff"] 52 | ``` 53 | 54 | The scopes are checked in the same way as the `scope_required` decorator. If no scopes are specified, all access tokens which have a scope are allowed access. Any and all scopes specified will be checked in order to access the view, and any missing scopes will result in the access token being denied. 55 | 56 | Insert application name here 57 | ---------------------------- 58 | Do you have an application that DOAC integrates with? We are accepting [pull requests](https://github.com/Rediker-Software/doac) to the documentation, which means you can add your information here. -------------------------------------------------------------------------------- /docs/markdown/models/index.md: -------------------------------------------------------------------------------- 1 | ====== 2 | Models 3 | ====== 4 | 5 | Django OAuth2 Consumer comes with multiple models which contain all of the tokens and other information that is used throughout the OAuth2 authorization process. 6 | 7 | ### *class* oauth2_consumer.models.Client ### 8 | 9 | A single client that can be used when requesting an authorization. 10 | 11 | name 12 | 13 | The name of the client. This will be used when the user is asked to approve any permissions that the client requests. 14 | 15 | secret 16 | 17 | The secret that is used to refresh tokens throughout the OAuth process. 18 | 19 | access_host 20 | 21 | The base URL that all `RedirectUri`'s will be validated against. 22 | 23 | is_active 24 | 25 | A boolean flag indicating whether or not the client can be used at all. 26 | 27 | generate_secret() 28 | 29 | Generates a secret string that meets the criteria of those which can be used for a client. 30 | 31 | save(*args, **kwargs) 32 | 33 | Saves the client to the database. A secret is automatically generated for the client and can be retrieved using `secret`. 34 | 35 | ### *class* oauth2_consumer.models.RedirectUri ### 36 | 37 | The url that a user can be redirected to during the authorization process. 38 | 39 | client 40 | 41 | The client that the url is tied to. It must be under the `~Client.access_host` of the `Client` in order to be used. 42 | 43 | url 44 | 45 | The url that can be used. It must be exactly the same when starting the authorization process. 46 | 47 | ### *class* oauth2_consumer.models.Scope ### 48 | 49 | A scope that can be requested by a client as a permission. 50 | 51 | short_name 52 | 53 | The name of the scope that is used when a client is requesting a set of scopes to be authorized for. 54 | 55 | full_name 56 | 57 | The full name of the scope, it will be used during the approval process when telling a user what the client is requesting. 58 | 59 | description 60 | 61 | A short description of exactly what the scope will give the client access to. 62 | -------------------------------------------------------------------------------- /docs/markdown/settings.md: -------------------------------------------------------------------------------- 1 | Settings 2 | ======== 3 | 4 | We think we set up Django OAuth2 Consumer with reasonable defualts, but there is always the option to change them through your central settings file. 5 | 6 | All of these settings are available on the `OAUTH_CONFIG` dictionary. 7 | 8 | `HANDLERS` 9 | ---------- 10 | 11 | This setting controls which handlers are acceptable for users to authenticate with. It should be specified as a tuple of strings which contain the full Python pathes to the middleware classes. If this is empty, users are not going to be able to authenticate with your project. 12 | 13 | *Default*: 14 | 15 | ``` 16 | "HANDLERS": ( 17 | "oauth2_consumer.handlers.bearer.BearerHandler", 18 | ) 19 | ``` 20 | 21 | `ACCESS_TOKEN` 22 | -------------- 23 | 24 | This setting controls the settings for access tokens. It should be a dictionary containing any of the following keys: 25 | 26 | ### `EXPIRES` 27 | 28 | A timedelta object representing the time after the creation of the token when the access token will expire and become invalid. 29 | 30 | *Default*: 31 | 32 | ``` 33 | datetime.timedelta(hours=2) 34 | ``` 35 | 36 | `AUTHORIZATION_CODE` 37 | -------------------- 38 | 39 | This setting controls the settings for the authorization code which is used during the authorization process. It should be a dictionary containing any of the following keys: 40 | 41 | ### `EXPIRES` 42 | 43 | A timedelta object representing the time after the creation of the code when the authorization code will expire and become invalid. 44 | 45 | *Default*: 46 | 47 | ``` 48 | datetime.timedelta(minutes=15) 49 | ``` 50 | 51 | `AUTHORIZATION_TOKEN` 52 | --------------------- 53 | 54 | This setting controls the settings for the authorization token which is used after the authorization process. It should be a dictionary containing any of the following keys: 55 | 56 | ### `EXPIRES` 57 | 58 | A timedelta object representing the time after the creation of the token when the authorization token will expire and become invalid. 59 | 60 | *Defualt*: 61 | 62 | ``` 63 | datetime.timedelta(minutes=15) 64 | ``` 65 | 66 | `REFRESH_TOKEN` 67 | --------------- 68 | 69 | This setting controls the settings for the refresh tokens which are used after the authorization process to retrieve access tokens. It should be a dictionary containing any of the folllwing keys: 70 | 71 | ### `EXPIRES` 72 | 73 | A timedelta object which represents the time after the creation of the token when the refresh token will expire and become invalid. 74 | 75 | *Default*: 76 | 77 | ``` 78 | datetime.timedelta(days=60) 79 | ``` 80 | -------------------------------------------------------------------------------- /docs/markdown/utilities.md: -------------------------------------------------------------------------------- 1 | DOAC Utilities 2 | ============== 3 | 4 | DOAC comes with a few utilities which make it easier to use DOAC. All of the utilities are located in the `utils.py` file. 5 | 6 | `doac.utils.`prune_old_authorization_codes() 7 | -------------------------------------------- 8 | Prunes all authorization codes which have expired. The codes may be pruned automatically if enabled within the settings. In this case, the codes will be automatically pruned each time that a user tries to authorize themselves. 9 | 10 | `doac.utils.`get_handler(`handler_name`) 11 | ---------------------------------------- 12 | Returns the class for the handler given the full path. It will automatically import the class from the given file. **Note:** the handler will only be imported if it is located within the specified list of handlers. 13 | 14 | `doac.utils.`request_error_header(`exception`) 15 | ---------------------------------------------- 16 | Generates the `WWW-Authenticate` header that must be supplied for errors that occur during various parts of the authorization and authentication process. 17 | 18 | `doac.utils.`total_seconds(`delta`) 19 | ----------------------------------- 20 | Returns the total number of seconds from a `timedelta`. 21 | -------------------------------------------------------------------------------- /docs/models/index.md: -------------------------------------------------------------------------------- 1 | ====== 2 | Models 3 | ====== 4 | 5 | Django OAuth2 Consumer comes with multiple models which contain all of the tokens and other information that is used throughout the OAuth2 authorization process. 6 | 7 | ### *class* oauth2_consumer.models.Client ### 8 | 9 | A single client that can be used when requesting an authorization. 10 | 11 | name 12 | 13 | The name of the client. This will be used when the user is asked to approve any permissions that the client requests. 14 | 15 | secret 16 | 17 | The secret that is used to refresh tokens throughout the OAuth process. 18 | 19 | access_host 20 | 21 | The base URL that all `RedirectUri`'s will be validated against. 22 | 23 | is_active 24 | 25 | A boolean flag indicating whether or not the client can be used at all. 26 | 27 | generate_secret() 28 | 29 | Generates a secret string that meets the criteria of those which can be used for a client. 30 | 31 | save(*args, **kwargs) 32 | 33 | Saves the client to the database. A secret is automatically generated for the client and can be retrieved using `secret`. 34 | 35 | ### *class* oauth2_consumer.models.RedirectUri ### 36 | 37 | The url that a user can be redirected to during the authorization process. 38 | 39 | client 40 | 41 | The client that the url is tied to. It must be under the `~Client.access_host` of the `Client` in order to be used. 42 | 43 | url 44 | 45 | The url that can be used. It must be exactly the same when starting the authorization process. 46 | 47 | ### *class* oauth2_consumer.models.Scope ### 48 | 49 | A scope that can be requested by a client as a permission. 50 | 51 | short_name 52 | 53 | The name of the scope that is used when a client is requesting a set of scopes to be authorized for. 54 | 55 | full_name 56 | 57 | The full name of the scope, it will be used during the approval process when telling a user what the client is requesting. 58 | 59 | description 60 | 61 | A short description of exactly what the scope will give the client access to. 62 | -------------------------------------------------------------------------------- /docs/settings.md: -------------------------------------------------------------------------------- 1 | Settings 2 | ======== 3 | 4 | We think we set up Django OAuth2 Consumer with reasonable defualts, but there is always the option to change them through your central settings file. 5 | 6 | All of these settings are available on the `OAUTH_CONFIG` dictionary. 7 | 8 | `HANDLERS` 9 | ---------- 10 | 11 | This setting controls which handlers are acceptable for users to authenticate with. It should be specified as a tuple of strings which contain the full Python pathes to the middleware classes. If this is empty, users are not going to be able to authenticate with your project. 12 | 13 | *Default*: 14 | 15 | ``` 16 | "HANDLERS": ( 17 | "oauth2_consumer.handlers.bearer.BearerHandler", 18 | ) 19 | ``` 20 | 21 | `ACCESS_TOKEN` 22 | -------------- 23 | 24 | This setting controls the settings for access tokens. It should be a dictionary containing any of the following keys: 25 | 26 | ### `EXPIRES` 27 | 28 | A timedelta object representing the time after the creation of the token when the access token will expire and become invalid. 29 | 30 | *Default*: 31 | 32 | ``` 33 | datetime.timedelta(hours=2) 34 | ``` 35 | 36 | `AUTHORIZATION_CODE` 37 | -------------------- 38 | 39 | This setting controls the settings for the authorization code which is used during the authorization process. It should be a dictionary containing any of the following keys: 40 | 41 | ### `EXPIRES` 42 | 43 | A timedelta object representing the time after the creation of the code when the authorization code will expire and become invalid. 44 | 45 | *Default*: 46 | 47 | ``` 48 | datetime.timedelta(minutes=15) 49 | ``` 50 | 51 | `AUTHORIZATION_TOKEN` 52 | --------------------- 53 | 54 | This setting controls the settings for the authorization token which is used after the authorization process. It should be a dictionary containing any of the following keys: 55 | 56 | ### `EXPIRES` 57 | 58 | A timedelta object representing the time after the creation of the token when the authorization token will expire and become invalid. 59 | 60 | *Defualt*: 61 | 62 | ``` 63 | datetime.timedelta(minutes=15) 64 | ``` 65 | 66 | `REFRESH_TOKEN` 67 | --------------- 68 | 69 | This setting controls the settings for the refresh tokens which are used after the authorization process to retrieve access tokens. It should be a dictionary containing any of the folllwing keys: 70 | 71 | ### `EXPIRES` 72 | 73 | A timedelta object which represents the time after the creation of the token when the refresh token will expire and become invalid. 74 | 75 | *Default*: 76 | 77 | ``` 78 | datetime.timedelta(days=60) 79 | ``` 80 | -------------------------------------------------------------------------------- /docs/utilities.md: -------------------------------------------------------------------------------- 1 | DOAC Utilities 2 | ============== 3 | 4 | DOAC comes with a few utilities which make it easier to use DOAC. All of the utilities are located in the `utils.py` file. 5 | 6 | `doac.utils.`prune_old_authorization_codes() 7 | -------------------------------------------- 8 | Prunes all authorization codes which have expired. The codes may be pruned automatically if enabled within the settings. In this case, the codes will be automatically pruned each time that a user tries to authorize themselves. 9 | 10 | `doac.utils.`get_handler(`handler_name`) 11 | ---------------------------------------- 12 | Returns the class for the handler given the full path. It will automatically import the class from the given file. **Note:** the handler will only be imported if it is located within the specified list of handlers. 13 | 14 | `doac.utils.`request_error_header(`exception`) 15 | ---------------------------------------------- 16 | Generates the `WWW-Authenticate` header that must be supplied for errors that occur during various parts of the authorization and authentication process. 17 | 18 | `doac.utils.`total_seconds(`delta`) 19 | ----------------------------------- 20 | Returns the total number of seconds from a `timedelta`. 21 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django>=1.3 2 | simplejson>=1.5 3 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from argparse import ArgumentParser 4 | import sys 5 | 6 | parser = ArgumentParser(description="Run the test suite.") 7 | 8 | parser.add_argument( 9 | "--failfast", 10 | action="store_true", 11 | default=False, 12 | dest="failfast", 13 | help="Stop the test suite after the first failed test.", 14 | ) 15 | 16 | parser.add_argument( 17 | "--no-coverage", 18 | action="store_false", 19 | default=True, 20 | dest="coverage", 21 | help="Do not run coverage.py while running the tests.", 22 | ) 23 | 24 | parser.add_argument( 25 | "--no-input", 26 | action="store_false", 27 | default=True, 28 | dest="interactive", 29 | help="If the tests require input, do not prompt the user for input.", 30 | ) 31 | 32 | args = parser.parse_args() 33 | 34 | if args.coverage: 35 | try: 36 | from coverage import coverage 37 | 38 | cov = coverage(include="doac*") 39 | cov.start() 40 | except ImportError: 41 | cov = None 42 | else: 43 | cov = None 44 | 45 | from django.conf import settings 46 | from tests import settings as test_settings 47 | 48 | settings.configure(test_settings, debug=True) 49 | 50 | from django.test.utils import get_runner 51 | 52 | TestRunner = get_runner(settings) 53 | 54 | runner = TestRunner(verbosity=1, interactive=args.interactive, failfast=args.failfast) 55 | 56 | failures = runner.run_tests(["tests", ]) 57 | 58 | if cov: 59 | cov.stop() 60 | cov.html_report() 61 | 62 | if failures: 63 | sys.exit(bool(failures)) 64 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [pep8] 2 | ignore==E128,E501,W293 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup, find_packages 4 | import os 5 | from doac import __version__ 6 | 7 | PACKAGE_DIR = os.path.abspath(os.path.dirname(__file__)) 8 | os.chdir(PACKAGE_DIR) 9 | 10 | 11 | setup(name='doac', 12 | version=__version__, 13 | url="https://github.com/kevin-brown/django-oauth2", 14 | author="Kevin Brown", 15 | author_email="kbrown@rediker.com", 16 | description="An OAuth2 Consumer application for your Django project.", 17 | long_description=file(os.path.join(PACKAGE_DIR, 'README.rst')).read(), 18 | license="MIT", 19 | packages=find_packages(exclude=["tests*", ]), 20 | include_package_data=True, 21 | install_requires=[ 22 | 'Django>=1.3', 23 | 'simplejson>=1.5', 24 | ], 25 | # See http://pypi.python.org/pypi?%3Aaction=list_classifiers 26 | classifiers=[ 27 | 'Development Status :: 3 - Alpha', 28 | 'Environment :: Web Environment', 29 | 'Framework :: Django', 30 | 'Intended Audience :: Developers', 31 | 'License :: OSI Approved :: MIT License', 32 | 'Programming Language :: Python', 33 | 'Programming Language :: Python :: 2.6', 34 | 'Programming Language :: Python :: 2.7', 35 | ] 36 | ) 37 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rediker-Software/doac/398fdd64452e4ff8662297b0381926addd77505a/tests/__init__.py -------------------------------------------------------------------------------- /tests/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rediker-Software/doac/398fdd64452e4ff8662297b0381926addd77505a/tests/models.py -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | # Import all of the settings from the global settings file. 2 | # This allows us to have our own custom settings for running tests. 3 | 4 | from django.conf.global_settings import * 5 | import os 6 | 7 | SITE_ROOT = os.path.dirname(os.path.realpath(__file__)) 8 | 9 | DEBUG = True 10 | TEMPLATE_DEBUG = True 11 | 12 | ROOT_URLCONF = "tests.urls" 13 | 14 | MIDDLEWARE_CLASSES = ( 15 | 'django.middleware.common.CommonMiddleware', 16 | 'django.contrib.sessions.middleware.SessionMiddleware', 17 | 'django.middleware.csrf.CsrfViewMiddleware', 18 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 19 | "doac.middleware.AuthenticationMiddleware", 20 | ) 21 | 22 | INSTALLED_APPS = [ 23 | "django.contrib.auth", 24 | 'django.contrib.contenttypes', 25 | 'django.contrib.sessions', 26 | "doac", 27 | "tests", 28 | ] 29 | 30 | DATABASES = { 31 | "default": { 32 | "ENGINE": "django.db.backends.sqlite3", 33 | "NAME": "oauth2.db", 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from contrib import * 2 | from integrations import * 3 | from handlers import * 4 | from models import * 5 | from views import * 6 | from .test_decorators import TestDecoratorErrors 7 | from .test_middleware import TestMiddleware 8 | -------------------------------------------------------------------------------- /tests/tests/contrib/__init__.py: -------------------------------------------------------------------------------- 1 | from .test_rest_framework import RestFrameworkTestCase 2 | -------------------------------------------------------------------------------- /tests/tests/contrib/test_rest_framework.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | try: 6 | # For Django ≥ 1.4. 7 | from django.conf.urls import patterns, url 8 | except ImportError: 9 | from django.conf.urls.defaults import patterns, url 10 | from django.http import HttpResponse 11 | from django.utils import unittest 12 | try: 13 | import rest_framework 14 | except ImportError: 15 | rest_framework = None 16 | if rest_framework: 17 | from rest_framework.permissions import IsAuthenticated 18 | from rest_framework.views import APIView 19 | 20 | from doac.contrib.rest_framework.authentication import DoacAuthentication 21 | from ..test_cases import TokenTestCase 22 | 23 | 24 | if rest_framework: 25 | class MockView(APIView): 26 | authentication_classes = (DoacAuthentication,) 27 | 28 | def get(self, request): 29 | return HttpResponse('foo') 30 | 31 | urlpatterns = patterns( 32 | '', 33 | url(r'^anonymous/$', MockView.as_view()), 34 | url(r'^authenticated-only/$', MockView.as_view(permission_classes=(IsAuthenticated,))), 35 | ) 36 | 37 | 38 | @unittest.skipUnless(rest_framework, 'Django Rest Framework is not installed.') 39 | class RestFrameworkTestCase(TokenTestCase): 40 | urls = 'tests.tests.contrib.test_rest_framework' 41 | 42 | def setUp(self): 43 | super(RestFrameworkTestCase, self).setUp() 44 | self.authorization_token.generate_refresh_token() 45 | self.access_token = self.authorization_token.refresh_token.generate_access_token() 46 | 47 | def test_anonymous_users_are_not_assaulted(self): 48 | """Ensure that if an user doesn’t make an attempt to authenticate, 401 Unauthorized isn’t returned.""" 49 | response = self.client.get('/anonymous/') 50 | self.assertEqual(response.status_code, 200) 51 | 52 | def test_anonymous_users_are_not_authenticated(self): 53 | """Ensure that authentication isn’t performed if an user doesn’t make an attempt to do it.""" 54 | response = self.client.get('/authenticated-only/') 55 | self.assertEqual(response.status_code, 401) 56 | 57 | def test_invalid_credentials_are_not_authenticated(self): 58 | """Ensure that 401 Unauthorized response is returned when user fails in authentication attempt.""" 59 | response = self.client.get('/anonymous/', HTTP_AUTHORIZATION='Bearer {0}'.format('invalid-token')) 60 | self.assertEqual(response.status_code, 401) 61 | 62 | def test_valid_credentials_are_authenticated(self): 63 | """Ensure that if user provides valid credentials, he is authorized.""" 64 | response = self.client.get('/authenticated-only/', HTTP_AUTHORIZATION='Bearer {0}'.format(self.access_token)) 65 | self.assertEqual(response.status_code, 200) 66 | -------------------------------------------------------------------------------- /tests/tests/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | from .test_bearer import TestBearerHandler 2 | -------------------------------------------------------------------------------- /tests/tests/handlers/test_bearer.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.test.client import RequestFactory 3 | from django.contrib.auth.models import User 4 | from doac.handlers.bearer import BearerHandler 5 | from doac.models import AuthorizationToken, Client, Scope 6 | from doac.utils import request_error_header 7 | 8 | 9 | class TestBearerHandler(TestCase): 10 | 11 | def setUp(self): 12 | self.oclient = Client(name="Test Client", access_host="http://localhost/") 13 | self.oclient.save() 14 | 15 | self.scope = Scope(short_name="test", full_name="Test Scope", description="Scope for testing") 16 | self.scope.save() 17 | 18 | self.user = User(username="Test", password="test", email="test@test.com") 19 | self.user.save() 20 | 21 | self.at = AuthorizationToken(client=self.oclient, user=self.user) 22 | self.at.save() 23 | self.at.scope = [self.scope] 24 | self.at.save() 25 | 26 | self.rt = self.at.generate_refresh_token() 27 | 28 | self.token = self.rt.generate_access_token() 29 | 30 | self.handler = BearerHandler() 31 | 32 | self.factory = RequestFactory() 33 | 34 | def test_access_token(self): 35 | request = self.factory.get("/") 36 | 37 | token = self.handler.access_token(self.token.token, request) 38 | 39 | self.assertEqual(token, self.token) 40 | 41 | token = self.handler.access_token("invalid", request) 42 | 43 | self.assertEqual(token, None) 44 | 45 | def test_authenticate(self): 46 | request = self.factory.get("/") 47 | 48 | user = self.handler.authenticate(self.token.token, request) 49 | 50 | self.assertEqual(user, self.user) 51 | 52 | user = self.handler.authenticate("invalid", request) 53 | 54 | self.assertEqual(user, None) 55 | 56 | def test_validate(self): 57 | from doac.exceptions.base import InvalidToken 58 | from doac.exceptions.invalid_request import CredentialsNotProvided 59 | 60 | request = self.factory.get("/") 61 | 62 | result = self.handler.validate(self.token.token, request) 63 | 64 | self.assertEqual(result, None) 65 | 66 | response = self.handler.validate("invalid", request) 67 | 68 | self.assertNotEqual(response, None) 69 | self.assertEqual(response.status_code, 401) 70 | self.assertEqual(response["WWW-Authenticate"], request_error_header(InvalidToken)) 71 | 72 | response = self.handler.validate("", request) 73 | 74 | self.assertNotEqual(response, None) 75 | self.assertEqual(response.status_code, 400) 76 | self.assertEqual(response["WWW-Authenticate"], request_error_header(CredentialsNotProvided)) 77 | -------------------------------------------------------------------------------- /tests/tests/integrations/__init__.py: -------------------------------------------------------------------------------- 1 | from .test_idan_oauthlib import TestOauthlib 2 | from .test_litl_rauth import TestRauth 3 | -------------------------------------------------------------------------------- /tests/tests/integrations/test_idan_oauthlib.py: -------------------------------------------------------------------------------- 1 | from django.core.urlresolvers import reverse 2 | from oauthlib.oauth2 import WebApplicationClient 3 | 4 | from doac.models import AuthorizationToken 5 | from ..test_cases import ApprovalTestCase 6 | 7 | 8 | class TestOauthlib(ApprovalTestCase): 9 | 10 | def setUp(self): 11 | super(TestOauthlib, self).setUp() 12 | 13 | self.libclient = WebApplicationClient(self.oauth_client.id) 14 | 15 | self.authorization_code.response_type = "code" 16 | self.authorization_code.save() 17 | 18 | def test_flow(self): 19 | self.client.login(username="test", password="test") 20 | 21 | request_uri = self.libclient.prepare_request_uri( 22 | "https://localhost" + reverse("oauth2_authorize"), 23 | redirect_uri=self.redirect_uri.url, 24 | scope=["test", ], 25 | state="test_state", 26 | ) 27 | 28 | response = self.client.get(request_uri[17:]) 29 | 30 | self.assertEqual(response.status_code, 200) 31 | self.assertTemplateUsed(response, "doac/authorize.html") 32 | 33 | approval_url = reverse("oauth2_approval") + "?code=" + self.authorization_code.token 34 | 35 | response = self.client.post(approval_url, { 36 | "code": self.authorization_code.token, 37 | "code_state": "test_state", 38 | "approve_access": None, 39 | }) 40 | 41 | response_uri = response.get("location", None) 42 | 43 | if not response_uri: 44 | response_uri = response.META["HTTP_LOCATION"] 45 | 46 | response_uri = response_uri.replace("http://", "https://") 47 | 48 | data = self.libclient.parse_request_uri_response(response_uri, state="test_state") 49 | 50 | authorization_token = data["code"] 51 | 52 | request_body = self.libclient.prepare_request_body( 53 | client_secret=self.oauth_client.secret, 54 | code=authorization_token, 55 | ) 56 | 57 | post_dict = {} 58 | 59 | for pair in request_body.split('&'): 60 | key, val = pair.split('=') 61 | 62 | post_dict[key] = val 63 | 64 | response = self.client.post(reverse("oauth2_token"), post_dict) 65 | 66 | data = self.libclient.parse_request_body_response(response.content) 67 | -------------------------------------------------------------------------------- /tests/tests/integrations/test_litl_rauth.py: -------------------------------------------------------------------------------- 1 | from django.core.urlresolvers import reverse 2 | from rauth import OAuth2Service 3 | 4 | from doac.models import AuthorizationToken 5 | from ..test_cases import ApprovalTestCase 6 | 7 | 8 | class TestRauth(ApprovalTestCase): 9 | 10 | def setUp(self): 11 | super(TestRauth, self).setUp() 12 | 13 | self.service = OAuth2Service( 14 | client_id=self.oauth_client.id, 15 | client_secret=self.oauth_client.secret, 16 | authorize_url=reverse("oauth2_authorize"), 17 | ) 18 | 19 | def test_flow(self): 20 | self.client.login(username="test", password="test") 21 | 22 | authorization_url = self.service.get_authorize_url(**{ 23 | "redirect_uri": self.redirect_uri.url, 24 | "response_type": "code", 25 | "scope": self.scope.short_name, 26 | "state": "test_state", 27 | }) 28 | 29 | response = self.client.get(authorization_url) 30 | 31 | self.assertEqual(response.status_code, 200) 32 | self.assertTemplateUsed(response, "doac/authorize.html") 33 | 34 | approval_url = reverse("oauth2_approval") + "?code=" + self.authorization_code.token 35 | 36 | response = self.client.post(approval_url, { 37 | "code": self.authorization_code.token, 38 | "code_state": "test_state", 39 | "approve_access": None, 40 | }) 41 | 42 | authorization_token = AuthorizationToken.objects.all()[0] 43 | -------------------------------------------------------------------------------- /tests/tests/mock.py: -------------------------------------------------------------------------------- 1 | class TestFunc: 2 | 3 | called = False 4 | 5 | def __call__(self): 6 | self.called = True 7 | -------------------------------------------------------------------------------- /tests/tests/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .test_authorization_token import TestAuthorizationTokenModel 2 | from .test_client import TestClientModel 3 | from .test_redirect_uri import TestRedirectUriModel 4 | -------------------------------------------------------------------------------- /tests/tests/models/test_authorization_token.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.contrib.auth.models import User 3 | from doac.models import AuthorizationToken, Client, RefreshToken, Scope 4 | 5 | 6 | class TestAuthorizationTokenModel(TestCase): 7 | 8 | def setUp(self): 9 | self.oclient = Client(name="Test Client", 10 | access_host="http://localhost/") 11 | self.oclient.save() 12 | 13 | self.scope = Scope(short_name="test", full_name="Test Scope", 14 | description="Scope for testing") 15 | self.scope.save() 16 | 17 | self.user = User(username="test", password="test", 18 | email="test@test.com") 19 | self.user.save() 20 | 21 | self.token = AuthorizationToken(client=self.oclient, user=self.user) 22 | self.token.save() 23 | 24 | self.token.scope = [self.scope] 25 | self.token.save() 26 | 27 | def test_unicode(self): 28 | self.assertEqual(unicode(self.token), self.token.token) 29 | 30 | def test_generate_refresh_token_creates(self): 31 | rt = self.token.generate_refresh_token() 32 | 33 | self.assertEqual(RefreshToken.objects.count(), 1) 34 | self.assertIsInstance(rt, RefreshToken) 35 | 36 | def test_generate_refresh_token_no_create_twice(self): 37 | self.token.generate_refresh_token() 38 | rt = self.token.generate_refresh_token() 39 | 40 | self.assertEqual(RefreshToken.objects.count(), 1) 41 | self.assertIsNone(rt) 42 | 43 | def test_generate_refresh_token_never_creates_twice(self): 44 | self.token.generate_refresh_token() 45 | self.token.is_active = True 46 | rt = self.token.generate_refresh_token() 47 | 48 | self.assertEqual(RefreshToken.objects.count(), 1) 49 | self.assertIsNone(rt) 50 | -------------------------------------------------------------------------------- /tests/tests/models/test_client.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from doac.models import Client 3 | 4 | 5 | class TestClientModel(TestCase): 6 | 7 | def test_unicode(self): 8 | client = Client(name="Test Client", access_host="http://localhost/") 9 | client.save() 10 | 11 | self.assertEqual(unicode(client), client.name) 12 | 13 | def test_generate_secret(self): 14 | client = Client(name="Test Client", access_host="http://localhost/") 15 | secret = client.generate_secret() 16 | 17 | self.assertEqual(len(secret), 50) 18 | -------------------------------------------------------------------------------- /tests/tests/models/test_redirect_uri.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from doac.models import Client, RedirectUri 3 | 4 | 5 | class TestRedirectUriModel(TestCase): 6 | 7 | def test_unicode(self): 8 | client = Client(name="Test Client", access_host="http://localhost/") 9 | client.save() 10 | 11 | uri = RedirectUri(client=client, 12 | url="http://localhost/redirect_endpoint") 13 | uri.save() 14 | 15 | self.assertEqual(unicode(uri), uri.url) 16 | -------------------------------------------------------------------------------- /tests/tests/test_cases.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from doac.models import AuthorizationCode, AuthorizationToken, Client, RedirectUri, Scope 3 | import urllib 4 | 5 | 6 | class OAuthTestCase(TestCase): 7 | 8 | def setUp(self): 9 | from django.contrib.auth.models import User 10 | 11 | self.user = User.objects.create_user("test", "test@test.com", "test") 12 | 13 | self.oauth_client = Client(name="Test Client", access_host="http://localhost/") 14 | self.oauth_client.save() 15 | 16 | self.scope = Scope(short_name="test", full_name="Test Scope", description="This is a test scope.") 17 | self.scope.save() 18 | 19 | def assertExceptionRendered(self, request, exception): 20 | self.assertEquals(request.content, exception.reason) 21 | self.assertEquals(request.status_code, 401) 22 | 23 | def assertExceptionJson(self, request, exception): 24 | try: 25 | import simplejson as json 26 | except ImportError: 27 | import json 28 | 29 | data = { 30 | "error": exception.error, 31 | "error_description": exception.reason, 32 | } 33 | 34 | self.assertEquals(request.content, json.dumps(data)) 35 | self.assertEquals(request.status_code, getattr(exception, 'code', 400)) 36 | 37 | def assertExceptionRedirect(self, request, exception): 38 | params = { 39 | "error": exception.error, 40 | "error_description": exception.reason, 41 | "state": "o2cs", 42 | } 43 | 44 | url = self.redirect_uri.url + "?" + urllib.urlencode(params) 45 | 46 | self.assertRedirects(request, url) 47 | self.assertEquals(request.status_code, 302) 48 | 49 | 50 | class ApprovalTestCase(OAuthTestCase): 51 | 52 | def setUp(self): 53 | super(ApprovalTestCase, self).setUp() 54 | 55 | self.redirect_uri = RedirectUri(client=self.oauth_client, url="http://localhost/redirect_endpoint/") 56 | self.redirect_uri.save() 57 | 58 | self.authorization_code = AuthorizationCode(client=self.oauth_client, redirect_uri=self.redirect_uri) 59 | self.authorization_code.save() 60 | 61 | self.authorization_code.scope = [self.scope] 62 | self.authorization_code.save() 63 | 64 | 65 | class AuthorizeTestCase(OAuthTestCase): 66 | 67 | def setUp(self): 68 | super(AuthorizeTestCase, self).setUp() 69 | 70 | self.redirect_uri = RedirectUri(client=self.oauth_client, url="http://localhost/redirect_endpoint/") 71 | self.redirect_uri.save() 72 | 73 | 74 | class TokenTestCase(OAuthTestCase): 75 | 76 | def setUp(self): 77 | super(TokenTestCase, self).setUp() 78 | 79 | self.client_secret = self.oauth_client.secret 80 | 81 | self.authorization_token = AuthorizationToken(user=self.user, client=self.oauth_client) 82 | self.authorization_token.save() 83 | 84 | self.authorization_token.scope = [self.scope] 85 | self.authorization_token.save() 86 | 87 | 88 | class DecoratorTestCase(OAuthTestCase): 89 | 90 | def setUp(self): 91 | from django.http import HttpRequest 92 | from doac.middleware import AuthenticationMiddleware 93 | 94 | super(DecoratorTestCase, self).setUp() 95 | 96 | self.client_secret = self.oauth_client.secret 97 | 98 | self.authorization_token = AuthorizationToken(user=self.user, client=self.oauth_client) 99 | self.authorization_token.save() 100 | 101 | self.authorization_token.scope = [self.scope] 102 | self.authorization_token.save() 103 | 104 | self.authorization_token.generate_refresh_token() 105 | self.authorization_token.refresh_token.generate_access_token() 106 | 107 | self.access_token = self.authorization_token.refresh_token.access_tokens.all()[0] 108 | 109 | self.request = HttpRequest() 110 | self.mw = AuthenticationMiddleware() 111 | 112 | 113 | class MiddlewareTestCase(OAuthTestCase): 114 | 115 | def setUp(self): 116 | from django.test.client import RequestFactory 117 | 118 | self.factory = RequestFactory() 119 | -------------------------------------------------------------------------------- /tests/tests/test_decorators.py: -------------------------------------------------------------------------------- 1 | from django.core.urlresolvers import reverse 2 | from django.http import HttpResponse 3 | from doac.exceptions.invalid_request import CredentialsNotProvided 4 | from doac.exceptions.insufficient_scope import ScopeNotEnough 5 | from doac.decorators import scope_required 6 | from doac.utils import request_error_header 7 | from .test_cases import DecoratorTestCase 8 | 9 | 10 | class TestDecoratorErrors(DecoratorTestCase): 11 | 12 | def test_no_args(self): 13 | @scope_required 14 | def no_args(request): 15 | return HttpResponse("success") 16 | 17 | response = no_args(self.request) 18 | 19 | self.assertEqual(response.status_code, 400) 20 | self.assertEqual(response["WWW-Authenticate"], request_error_header(CredentialsNotProvided)) 21 | 22 | request = self.request 23 | request.META["HTTP_AUTHORIZATION"] = "Bearer %s" % (self.access_token.token, ) 24 | self.mw.process_request(request) 25 | 26 | response = no_args(request) 27 | 28 | self.assertEqual(response.status_code, 200) 29 | self.assertEqual(response.content, "success") 30 | 31 | def test_no_args_with_parens(self): 32 | @scope_required() 33 | def no_args(request): 34 | return HttpResponse("success") 35 | 36 | response = no_args(self.request) 37 | 38 | self.assertEqual(response.status_code, 400) 39 | self.assertEqual(response["WWW-Authenticate"], request_error_header(CredentialsNotProvided)) 40 | 41 | request = self.request 42 | request.META["HTTP_AUTHORIZATION"] = "Bearer %s" % (self.access_token.token, ) 43 | self.mw.process_request(request) 44 | 45 | response = no_args(request) 46 | 47 | self.assertEqual(response.status_code, 200) 48 | self.assertEqual(response.content, "success") 49 | 50 | def test_has_scope(self): 51 | @scope_required("test") 52 | def has_scope(request): 53 | return HttpResponse("success") 54 | 55 | response = has_scope(self.request) 56 | 57 | self.assertEqual(response.status_code, 400) 58 | self.assertEqual(response["WWW-Authenticate"], request_error_header(CredentialsNotProvided)) 59 | 60 | request = self.request 61 | request.META["HTTP_AUTHORIZATION"] = "Bearer %s" % (self.access_token.token, ) 62 | self.mw.process_request(request) 63 | 64 | response = has_scope(request) 65 | 66 | self.assertEqual(response.status_code, 200) 67 | self.assertEqual(response.content, "success") 68 | 69 | def test_scope_doesnt_exist(self): 70 | @scope_required("invalid") 71 | def scope_doesnt_exist(request): 72 | return HttpResponse("success") 73 | 74 | response = scope_doesnt_exist(self.request) 75 | 76 | self.assertEqual(response.status_code, 400) 77 | self.assertEqual(response["WWW-Authenticate"], request_error_header(CredentialsNotProvided)) 78 | 79 | request = self.request 80 | request.META["HTTP_AUTHORIZATION"] = "Bearer %s" % (self.access_token.token, ) 81 | self.mw.process_request(request) 82 | 83 | response = scope_doesnt_exist(request) 84 | 85 | self.assertEqual(response.status_code, 403) 86 | self.assertEqual(response["WWW-Authenticate"], request_error_header(ScopeNotEnough)) 87 | 88 | def test_doesnt_have_all_scope(self): 89 | @scope_required("test", "invalid") 90 | def doesnt_have_all_scope(request): 91 | return HttpResponse("success") 92 | 93 | response = doesnt_have_all_scope(self.request) 94 | 95 | self.assertEqual(response.status_code, 400) 96 | self.assertEqual(response["WWW-Authenticate"], request_error_header(CredentialsNotProvided)) 97 | 98 | request = self.request 99 | request.META["HTTP_AUTHORIZATION"] = "Bearer %s" % (self.access_token.token, ) 100 | self.mw.process_request(request) 101 | 102 | response = doesnt_have_all_scope(self.request) 103 | 104 | self.assertEqual(response.status_code, 403) 105 | self.assertEqual(response["WWW-Authenticate"], request_error_header(ScopeNotEnough)) 106 | -------------------------------------------------------------------------------- /tests/tests/test_middleware.py: -------------------------------------------------------------------------------- 1 | from doac.middleware import AuthenticationMiddleware 2 | from .test_cases import MiddlewareTestCase 3 | 4 | 5 | class TestMiddleware(MiddlewareTestCase): 6 | 7 | def test_no_token(self): 8 | request = self.factory.get("/") 9 | 10 | AuthenticationMiddleware().process_request(request) 11 | 12 | self.assertEqual(request.auth_type, None) 13 | self.assertFalse(hasattr(request, "acess_token")) 14 | self.assertFalse(hasattr(request, "user")) 15 | 16 | def test_invalid_handler(self): 17 | request = self.factory.get("/") 18 | request.META["HTTP_AUTHORIZATION"] = "type token" 19 | 20 | try: 21 | AuthenticationMiddleware().process_request(request) 22 | 23 | self.fail("Request was not rejected") 24 | except Exception: 25 | pass 26 | 27 | #self.assertEqual(request.auth_type, "type") 28 | 29 | def test_invalid_bearer_token(self): 30 | request = self.factory.get("/") 31 | request.META["HTTP_AUTHORIZATION"] = "bearer invalid" 32 | 33 | try: 34 | AuthenticationMiddleware().process_request(request) 35 | 36 | self.fail("Token was not rejected") 37 | except Exception: 38 | pass 39 | 40 | #self.assertEqual(request.auth_type, "bearer") 41 | -------------------------------------------------------------------------------- /tests/tests/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .test_approval import TestApprovalErrors, TestApprovalResponse 2 | from .test_authorize import TestAuthorizeErrors, TestAuthorizeResponse 3 | from .test_token import TestTokenErrors, TestTokenResponse 4 | -------------------------------------------------------------------------------- /tests/tests/views/test_approval.py: -------------------------------------------------------------------------------- 1 | from django.core.urlresolvers import reverse 2 | from ..test_cases import ApprovalTestCase 3 | 4 | 5 | class TestApprovalErrors(ApprovalTestCase): 6 | 7 | def test_code(self): 8 | from doac.exceptions.invalid_request import AuthorizationCodeNotValid, AuthorizationCodeNotProvided 9 | 10 | request = self.client.post(reverse("oauth2_approval")) 11 | self.assertExceptionRendered(request, AuthorizationCodeNotProvided()) 12 | 13 | request = self.client.post(reverse("oauth2_approval") + "?code=invalid") 14 | self.assertExceptionRendered(request, AuthorizationCodeNotProvided()) 15 | 16 | request = self.client.post(reverse("oauth2_approval"), {"code": "invalid"}) 17 | self.assertExceptionRendered(request, AuthorizationCodeNotValid()) 18 | 19 | data = { 20 | "code": "invalid", 21 | } 22 | 23 | request = self.client.post(reverse("oauth2_approval") + "?code=%s" % (self.authorization_code.token, ), data) 24 | self.assertExceptionRendered(request, AuthorizationCodeNotValid()) 25 | 26 | data = { 27 | "code": self.authorization_code.token, 28 | } 29 | 30 | request = self.client.post(reverse("oauth2_approval") + "?code=invalid", data) 31 | self.assertExceptionRendered(request, AuthorizationCodeNotValid()) 32 | 33 | request = self.client.post(reverse("oauth2_approval") + "?code=invalid", {"code": "invalid"}) 34 | self.assertExceptionRendered(request, AuthorizationCodeNotValid()) 35 | 36 | 37 | class TestApprovalResponse(ApprovalTestCase): 38 | 39 | def test_denied(self): 40 | from doac.exceptions.access_denied import AuthorizationDenied 41 | 42 | data = { 43 | "code": self.authorization_code.token, 44 | "code_state": "o2cs", 45 | "deny_access": None, 46 | } 47 | 48 | request = self.client.post(reverse("oauth2_approval") + "?code=%s" % (self.authorization_code.token, ), data) 49 | self.assertExceptionRedirect(request, AuthorizationDenied()) 50 | 51 | def test_approved_code(self): 52 | from doac.models import AuthorizationToken 53 | import urllib 54 | 55 | self.client.login(username="test", password="test") 56 | 57 | data = { 58 | "code": self.authorization_code.token, 59 | "code_state": "o2cs", 60 | "approve_access": None, 61 | } 62 | 63 | self.authorization_code.response_type = "code" 64 | self.authorization_code.save() 65 | 66 | request = self.client.post(reverse("oauth2_approval") + "?code=%s" % (self.authorization_code.token, ), data) 67 | self.assertEqual(request.status_code, 302) 68 | 69 | args = { 70 | "state": "o2cs", 71 | "code": AuthorizationToken.objects.all()[0].token, 72 | } 73 | self.assertRedirects(request, self.redirect_uri.url + "?" + urllib.urlencode(args)) 74 | 75 | def test_approved_token(self): 76 | from doac.models import AccessToken 77 | import urllib 78 | 79 | self.client.login(username="test", password="test") 80 | 81 | data = { 82 | "code": self.authorization_code.token, 83 | "code_state": "o2cs", 84 | "approve_access": None, 85 | } 86 | 87 | self.authorization_code.response_type = "token" 88 | self.authorization_code.save() 89 | 90 | request = self.client.post(reverse("oauth2_approval") + "?code=%s" % (self.authorization_code.token, ), data) 91 | self.assertEqual(request.status_code, 302) 92 | 93 | args = { 94 | "state": "o2cs", 95 | "access_token": AccessToken.objects.all()[0].token, 96 | } 97 | self.assertRedirects(request, self.redirect_uri.url + "#" + urllib.urlencode(args)) 98 | -------------------------------------------------------------------------------- /tests/tests/views/test_authorize.py: -------------------------------------------------------------------------------- 1 | from django.core.urlresolvers import reverse 2 | from doac.models import AuthorizationCode, Client, RedirectUri, Scope 3 | from ..test_cases import AuthorizeTestCase 4 | 5 | 6 | class TestAuthorizeErrors(AuthorizeTestCase): 7 | 8 | def test_client_id(self): 9 | from doac.exceptions.invalid_request import ClientNotProvided 10 | from doac.exceptions.invalid_client import ClientDoesNotExist 11 | 12 | request = self.client.get(reverse("oauth2_authorize")) 13 | self.assertExceptionRendered(request, ClientNotProvided()) 14 | 15 | request = self.client.get(reverse("oauth2_authorize") + "?client_id=") 16 | self.assertExceptionRendered(request, ClientNotProvided()) 17 | 18 | request = self.client.get(reverse("oauth2_authorize") + "?client_id=1234") 19 | self.assertExceptionRendered(request, ClientDoesNotExist()) 20 | 21 | request = self.client.get(reverse("oauth2_authorize") + "?client_id=foo") 22 | self.assertExceptionRendered(request, ClientDoesNotExist()) 23 | 24 | def test_redirect_uri(self): 25 | from doac.exceptions.invalid_request import RedirectUriNotProvided, RedirectUriDoesNotValidate 26 | 27 | request = self.client.get(reverse("oauth2_authorize") + "?client_id=%s" % (self.oauth_client.id, )) 28 | self.assertExceptionRendered(request, RedirectUriNotProvided()) 29 | 30 | request = self.client.get(reverse("oauth2_authorize") + "?client_id=%s&redirect_uri=" % (self.oauth_client.id, )) 31 | self.assertExceptionRendered(request, RedirectUriNotProvided()) 32 | 33 | request = self.client.get(reverse("oauth2_authorize") + "?client_id=%s&redirect_uri=invalid" % (self.oauth_client.id, )) 34 | self.assertExceptionRendered(request, RedirectUriDoesNotValidate()) 35 | 36 | request = self.client.get(reverse("oauth2_authorize") + "?client_id=%s&redirect_uri=http://localhost/invalid" % (self.oauth_client.id, )) 37 | self.assertExceptionRendered(request, RedirectUriDoesNotValidate()) 38 | 39 | def test_scope(self): 40 | from doac.exceptions.invalid_scope import ScopeNotProvided, ScopeNotValid 41 | 42 | request = self.client.get(reverse("oauth2_authorize") + "?client_id=%s&redirect_uri=%s" % (self.oauth_client.id, self.redirect_uri.url, )) 43 | self.assertExceptionRedirect(request, ScopeNotProvided()) 44 | 45 | request = self.client.get(reverse("oauth2_authorize") + "?client_id=%s&redirect_uri=%s&scope=" % (self.oauth_client.id, self.redirect_uri.url, )) 46 | self.assertExceptionRedirect(request, ScopeNotProvided()) 47 | 48 | request = self.client.get(reverse("oauth2_authorize") + "?client_id=%s&redirect_uri=%s&scope=invalid" % (self.oauth_client.id, self.redirect_uri.url, )) 49 | self.assertExceptionRedirect(request, ScopeNotValid()) 50 | 51 | def test_response_type(self): 52 | from doac.exceptions.invalid_request import ResponseTypeNotProvided 53 | from doac.exceptions.unsupported_response_type import ResponseTypeNotValid 54 | 55 | request = self.client.get(reverse("oauth2_authorize") + "?client_id=%s&redirect_uri=%s&scope=%s" % (self.oauth_client.id, self.redirect_uri.url, self.scope.short_name, )) 56 | self.assertExceptionRedirect(request, ResponseTypeNotProvided()) 57 | 58 | request = self.client.get(reverse("oauth2_authorize") + "?client_id=%s&redirect_uri=%s&scope=%s&response_type=" % (self.oauth_client.id, self.redirect_uri.url, self.scope.short_name, )) 59 | self.assertExceptionRedirect(request, ResponseTypeNotProvided()) 60 | 61 | request = self.client.get(reverse("oauth2_authorize") + "?client_id=%s&redirect_uri=%s&scope=%s&response_type=invalid" % (self.oauth_client.id, self.redirect_uri.url, self.scope.short_name, )) 62 | self.assertExceptionRedirect(request, ResponseTypeNotValid()) 63 | 64 | 65 | class TestAuthorizeResponse(AuthorizeTestCase): 66 | 67 | def test_login_redirect(self): 68 | auth_url = reverse("oauth2_authorize") + "?client_id=%s&redirect_uri=%s&scope=%s&response_type=token" % (self.oauth_client.id, self.redirect_uri.url, self.scope.short_name, ) 69 | 70 | request = self.client.get(auth_url) 71 | 72 | self.assertEqual(request.status_code, 302) 73 | 74 | def test_approval_form(self): 75 | self.client.login(username="test", password="test") 76 | 77 | for response_type in ["token", "code", ]: 78 | request = self.client.get(reverse("oauth2_authorize") + "?client_id=%s&redirect_uri=%s&scope=%s&response_type=code" % (self.oauth_client.id, self.redirect_uri.url, self.scope.short_name, )) 79 | 80 | self.assertTemplateUsed(request, "doac/authorize.html") 81 | 82 | self.assertEqual(request.context["authorization_code"], AuthorizationCode.objects.latest("id")) 83 | self.assertEqual(request.context["client"], self.oauth_client) 84 | self.assertEqual(request.context["scopes"], [self.scope]) 85 | self.assertEqual(request.context["state"], "o2cs") 86 | 87 | self.assertEqual(AuthorizationCode.objects.count(), 2) 88 | -------------------------------------------------------------------------------- /tests/tests/views/test_token.py: -------------------------------------------------------------------------------- 1 | from django.core.urlresolvers import reverse 2 | from ..test_cases import TokenTestCase 3 | try: 4 | import simplejson as json 5 | except ImportError: 6 | import json 7 | 8 | 9 | class TestTokenErrors(TokenTestCase): 10 | 11 | def test_grant_type(self): 12 | from doac.exceptions.unsupported_grant_type import GrantTypeNotProvided, GrantTypeNotValid 13 | 14 | request = self.client.post(reverse("oauth2_token")) 15 | self.assertExceptionJson(request, GrantTypeNotProvided()) 16 | 17 | request = self.client.post(reverse("oauth2_token"), {"grant_type": ""}) 18 | self.assertExceptionJson(request, GrantTypeNotProvided()) 19 | 20 | request = self.client.post(reverse("oauth2_token"), {"grant_type": "invalid"}) 21 | self.assertExceptionJson(request, GrantTypeNotValid()) 22 | 23 | def test_client_id(self): 24 | from doac.exceptions.invalid_request import ClientNotProvided 25 | from doac.exceptions.invalid_client import ClientDoesNotExist 26 | 27 | request = self.client.post(reverse("oauth2_token"), {"grant_type": "authorization_code"}) 28 | self.assertExceptionJson(request, ClientNotProvided()) 29 | 30 | request = self.client.post(reverse("oauth2_token"), {"grant_type": "authorization_code", "client_id": ""}) 31 | self.assertExceptionJson(request, ClientNotProvided()) 32 | 33 | request = self.client.post(reverse("oauth2_token"), {"grant_type": "authorization_code", "client_id": 1234}) 34 | self.assertExceptionJson(request, ClientDoesNotExist()) 35 | 36 | def test_client_secret(self): 37 | from doac.exceptions.invalid_client import ClientSecretNotValid 38 | from doac.exceptions.invalid_request import ClientSecretNotProvided 39 | 40 | request = self.client.post(reverse("oauth2_token"), {"grant_type": "authorization_code", "client_id": self.oauth_client.id}) 41 | self.assertExceptionJson(request, ClientSecretNotProvided()) 42 | 43 | request = self.client.post(reverse("oauth2_token"), {"grant_type": "authorization_code", "client_id": self.oauth_client.id, "client_secret": ""}) 44 | self.assertExceptionJson(request, ClientSecretNotProvided()) 45 | 46 | request = self.client.post(reverse("oauth2_token"), {"grant_type": "authorization_code", "client_id": self.oauth_client.id, "client_secret": "notVerySecret"}) 47 | self.assertExceptionJson(request, ClientSecretNotValid()) 48 | 49 | def test_code(self): 50 | from doac.exceptions.invalid_request import AuthorizationCodeAlreadyUsed, AuthorizationCodeNotProvided, AuthorizationCodeNotValid 51 | 52 | request = self.client.post(reverse("oauth2_token"), {"grant_type": "authorization_code", "client_id": self.oauth_client.id, "client_secret": self.client_secret}) 53 | self.assertExceptionJson(request, AuthorizationCodeNotProvided()) 54 | 55 | request = self.client.post(reverse("oauth2_token"), {"grant_type": "authorization_code", "client_id": self.oauth_client.id, "client_secret": self.client_secret, "code": ""}) 56 | self.assertExceptionJson(request, AuthorizationCodeNotProvided()) 57 | 58 | request = self.client.post(reverse("oauth2_token"), {"grant_type": "authorization_code", "client_id": self.oauth_client.id, "client_secret": self.client_secret, "code": "invalid"}) 59 | self.assertExceptionJson(request, AuthorizationCodeNotValid()) 60 | 61 | request = self.client.post(reverse("oauth2_token"), {"grant_type": "authorization_code", "client_id": self.oauth_client.id, "client_secret": self.client_secret, "code": self.authorization_token.token}) 62 | request = self.client.post(reverse("oauth2_token"), {"grant_type": "authorization_code", "client_id": self.oauth_client.id, "client_secret": self.client_secret, "code": self.authorization_token.token}) 63 | self.assertExceptionJson(request, AuthorizationCodeAlreadyUsed()) 64 | 65 | def test_refresh_token(self): 66 | from doac.exceptions.invalid_request import RefreshTokenNotProvided, RefreshTokenNotValid 67 | 68 | request = self.client.post(reverse("oauth2_token"), {"grant_type": "refresh_token", "client_id": self.oauth_client.id, "client_secret": self.client_secret}) 69 | self.assertExceptionJson(request, RefreshTokenNotProvided()) 70 | 71 | request = self.client.post(reverse("oauth2_token"), {"grant_type": "refresh_token", "client_id": self.oauth_client.id, "client_secret": self.client_secret, "refresh_token": ""}) 72 | self.assertExceptionJson(request, RefreshTokenNotProvided()) 73 | 74 | request = self.client.post(reverse("oauth2_token"), {"grant_type": "refresh_token", "client_id": self.oauth_client.id, "client_secret": self.client_secret, "refresh_token": "invalid"}) 75 | self.assertExceptionJson(request, RefreshTokenNotValid()) 76 | 77 | 78 | class TestTokenResponse(TokenTestCase): 79 | 80 | def test_authorization_token(self): 81 | data = { 82 | "grant_type": "authorization_code", 83 | "client_id": self.oauth_client.id, 84 | "client_secret": self.client_secret, 85 | "code": self.authorization_token.token, 86 | } 87 | 88 | request = self.client.post(reverse("oauth2_token"), data) 89 | 90 | response = { 91 | "refresh_token": self.authorization_token.refresh_token.token, 92 | "token_type": "bearer", 93 | "expires_in": 5183999, 94 | "access_token": self.authorization_token.refresh_token.access_tokens.all()[0].token, 95 | } 96 | 97 | self.assertEqual(request.content, json.dumps(response)) 98 | self.assertEqual(request.status_code, 200) 99 | 100 | def test_refresh_token(self): 101 | refresh_token = self.authorization_token.generate_refresh_token() 102 | 103 | data = { 104 | "grant_type": "refresh_token", 105 | "client_id": self.oauth_client.id, 106 | "client_secret": self.client_secret, 107 | "refresh_token": refresh_token.token, 108 | } 109 | 110 | request = self.client.post(reverse("oauth2_token"), data) 111 | 112 | response = { 113 | "token_type": "bearer", 114 | "expires_in": 7199, 115 | "access_token": refresh_token.access_tokens.all()[0].token, 116 | } 117 | 118 | self.assertEqual(request.content, json.dumps(response)) 119 | self.assertEqual(request.status_code, 200) 120 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | try: 2 | from django.conf.urls import include, patterns, url 3 | except ImportError: 4 | from django.conf.urls.defaults import include, patterns, url 5 | 6 | from doac import urls as oauth_urls 7 | from . import views 8 | 9 | urlpatterns = patterns('', 10 | url(r"^oauth/", include(oauth_urls)), 11 | url(r"^auth/", include("django.contrib.auth.urls")), 12 | 13 | url(r"^no_args/", views.no_args, name="no_args"), 14 | url(r"^has_scope/", views.has_scope, name="has_scope"), 15 | url(r"^scope_doesnt_exist/", views.scope_doesnt_exist, name="scope_doesnt_exist"), 16 | url(r"^doesnt_have_all_scope/", views.doesnt_have_all_scope, name="doesnt_have_all_scope"), 17 | 18 | url(r"^redirect_endpoint/", views.redirect_endpoint, name="redirect_endpoint"), 19 | ) 20 | -------------------------------------------------------------------------------- /tests/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from doac.decorators import scope_required 3 | 4 | @scope_required() 5 | def no_args(request): 6 | return HttpResponse("success") 7 | 8 | @scope_required("test") 9 | def has_scope(request): 10 | return HttpResponse("success") 11 | 12 | @scope_required("invalid") 13 | def scope_doesnt_exist(request): 14 | return HttpResponse("success") 15 | 16 | @scope_required("test", "invalid") 17 | def doesnt_have_all_scope(request): 18 | return HttpResponse("success") 19 | 20 | 21 | def redirect_endpoint(request): 22 | return HttpResponse(repr(dict(request.GET))) 23 | --------------------------------------------------------------------------------