mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-21 22:51:08 -05:00
first commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
__version__ = "3.0.1"
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
77
venv/lib/python3.11/site-packages/oauth2_provider/admin.py
Normal file
77
venv/lib/python3.11/site-packages/oauth2_provider/admin.py
Normal file
@@ -0,0 +1,77 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from oauth2_provider.models import (
|
||||
get_access_token_admin_class,
|
||||
get_access_token_model,
|
||||
get_application_admin_class,
|
||||
get_application_model,
|
||||
get_grant_admin_class,
|
||||
get_grant_model,
|
||||
get_id_token_admin_class,
|
||||
get_id_token_model,
|
||||
get_refresh_token_admin_class,
|
||||
get_refresh_token_model,
|
||||
)
|
||||
|
||||
|
||||
has_email = hasattr(get_user_model(), "email")
|
||||
|
||||
|
||||
class ApplicationAdmin(admin.ModelAdmin):
|
||||
list_display = ("pk", "name", "user", "client_type", "authorization_grant_type")
|
||||
list_filter = ("client_type", "authorization_grant_type", "skip_authorization")
|
||||
radio_fields = {
|
||||
"client_type": admin.HORIZONTAL,
|
||||
"authorization_grant_type": admin.VERTICAL,
|
||||
}
|
||||
search_fields = ("name",) + (("user__email",) if has_email else ())
|
||||
raw_id_fields = ("user",)
|
||||
|
||||
|
||||
class AccessTokenAdmin(admin.ModelAdmin):
|
||||
list_display = ("token", "user", "application", "expires")
|
||||
list_select_related = ("application", "user")
|
||||
raw_id_fields = ("user", "source_refresh_token")
|
||||
search_fields = ("token",) + (("user__email",) if has_email else ())
|
||||
list_filter = ("application",)
|
||||
|
||||
|
||||
class GrantAdmin(admin.ModelAdmin):
|
||||
list_display = ("code", "application", "user", "expires")
|
||||
raw_id_fields = ("user",)
|
||||
search_fields = ("code",) + (("user__email",) if has_email else ())
|
||||
|
||||
|
||||
class IDTokenAdmin(admin.ModelAdmin):
|
||||
list_display = ("jti", "user", "application", "expires")
|
||||
raw_id_fields = ("user",)
|
||||
search_fields = ("user__email",) if has_email else ()
|
||||
list_filter = ("application",)
|
||||
list_select_related = ("application", "user")
|
||||
|
||||
|
||||
class RefreshTokenAdmin(admin.ModelAdmin):
|
||||
list_display = ("token", "user", "application")
|
||||
raw_id_fields = ("user", "access_token")
|
||||
search_fields = ("token",) + (("user__email",) if has_email else ())
|
||||
list_filter = ("application",)
|
||||
|
||||
|
||||
application_model = get_application_model()
|
||||
access_token_model = get_access_token_model()
|
||||
grant_model = get_grant_model()
|
||||
id_token_model = get_id_token_model()
|
||||
refresh_token_model = get_refresh_token_model()
|
||||
|
||||
application_admin_class = get_application_admin_class()
|
||||
access_token_admin_class = get_access_token_admin_class()
|
||||
grant_admin_class = get_grant_admin_class()
|
||||
id_token_admin_class = get_id_token_admin_class()
|
||||
refresh_token_admin_class = get_refresh_token_admin_class()
|
||||
|
||||
admin.site.register(application_model, application_admin_class)
|
||||
admin.site.register(access_token_model, access_token_admin_class)
|
||||
admin.site.register(grant_model, grant_admin_class)
|
||||
admin.site.register(id_token_model, id_token_admin_class)
|
||||
admin.site.register(refresh_token_model, refresh_token_admin_class)
|
||||
10
venv/lib/python3.11/site-packages/oauth2_provider/apps.py
Normal file
10
venv/lib/python3.11/site-packages/oauth2_provider/apps.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class DOTConfig(AppConfig):
|
||||
name = "oauth2_provider"
|
||||
verbose_name = "Django OAuth Toolkit"
|
||||
|
||||
def ready(self):
|
||||
# Import checks to ensure they run.
|
||||
from . import checks # noqa: F401
|
||||
@@ -0,0 +1,35 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
|
||||
from .oauth2_backends import get_oauthlib_core
|
||||
|
||||
|
||||
UserModel = get_user_model()
|
||||
OAuthLibCore = get_oauthlib_core()
|
||||
|
||||
|
||||
class OAuth2Backend:
|
||||
"""
|
||||
Authenticate against an OAuth2 access token
|
||||
"""
|
||||
|
||||
def authenticate(self, request=None, **credentials):
|
||||
if request is not None:
|
||||
try:
|
||||
valid, request = OAuthLibCore.verify_request(request, scopes=[])
|
||||
except ValueError as error:
|
||||
if str(error) == "Invalid hex encoding in query string.":
|
||||
raise SuspiciousOperation(error)
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
if valid:
|
||||
return request.user
|
||||
|
||||
return None
|
||||
|
||||
def get_user(self, user_id):
|
||||
try:
|
||||
return UserModel.objects.get(pk=user_id)
|
||||
except UserModel.DoesNotExist:
|
||||
return None
|
||||
28
venv/lib/python3.11/site-packages/oauth2_provider/checks.py
Normal file
28
venv/lib/python3.11/site-packages/oauth2_provider/checks.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from django.apps import apps
|
||||
from django.core import checks
|
||||
from django.db import router
|
||||
|
||||
from .settings import oauth2_settings
|
||||
|
||||
|
||||
@checks.register(checks.Tags.database)
|
||||
def validate_token_configuration(app_configs, **kwargs):
|
||||
databases = set(
|
||||
router.db_for_write(apps.get_model(model))
|
||||
for model in (
|
||||
oauth2_settings.ACCESS_TOKEN_MODEL,
|
||||
oauth2_settings.ID_TOKEN_MODEL,
|
||||
oauth2_settings.REFRESH_TOKEN_MODEL,
|
||||
)
|
||||
)
|
||||
|
||||
# This is highly unlikely, but let's warn people just in case it does.
|
||||
# If the tokens were allowed to be in different databases this would require all
|
||||
# writes to have a transaction around each database. Instead, let's enforce that
|
||||
# they all live together in one database.
|
||||
# The tokens are not required to live in the default database provided the Django
|
||||
# routers know the correct database for them.
|
||||
if len(databases) > 1:
|
||||
return [checks.Error("The token models are expected to be stored in the same database.")]
|
||||
|
||||
return []
|
||||
15
venv/lib/python3.11/site-packages/oauth2_provider/compat.py
Normal file
15
venv/lib/python3.11/site-packages/oauth2_provider/compat.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
The `compat` module provides support for backwards compatibility with older
|
||||
versions of Django and Python.
|
||||
"""
|
||||
|
||||
try:
|
||||
# Django 5.1 introduced LoginRequiredMiddleware, and login_not_required decorator
|
||||
from django.contrib.auth.decorators import login_not_required
|
||||
except ImportError:
|
||||
|
||||
def login_not_required(view_func):
|
||||
return view_func
|
||||
|
||||
|
||||
__all__ = ["login_not_required"]
|
||||
Binary file not shown.
@@ -0,0 +1,9 @@
|
||||
# flake8: noqa
|
||||
from .authentication import OAuth2Authentication
|
||||
from .permissions import (
|
||||
IsAuthenticatedOrTokenHasScope,
|
||||
TokenHasReadWriteScope,
|
||||
TokenHasResourceScope,
|
||||
TokenHasScope,
|
||||
TokenMatchesOASRequirements,
|
||||
)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,55 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from rest_framework.authentication import BaseAuthentication
|
||||
|
||||
from ...oauth2_backends import get_oauthlib_core
|
||||
|
||||
|
||||
class OAuth2Authentication(BaseAuthentication):
|
||||
"""
|
||||
OAuth 2 authentication backend using `django-oauth-toolkit`
|
||||
"""
|
||||
|
||||
www_authenticate_realm = "api"
|
||||
|
||||
def _dict_to_string(self, my_dict):
|
||||
"""
|
||||
Return a string of comma-separated key-value pairs (e.g. k="v",k2="v2").
|
||||
"""
|
||||
return ",".join(['{k}="{v}"'.format(k=k, v=v) for k, v in my_dict.items()])
|
||||
|
||||
def authenticate(self, request):
|
||||
"""
|
||||
Returns two-tuple of (user, token) if authentication succeeds,
|
||||
or None otherwise.
|
||||
"""
|
||||
if request is None:
|
||||
return None
|
||||
oauthlib_core = get_oauthlib_core()
|
||||
try:
|
||||
valid, r = oauthlib_core.verify_request(request, scopes=[])
|
||||
except ValueError as error:
|
||||
if str(error) == "Invalid hex encoding in query string.":
|
||||
raise SuspiciousOperation(error)
|
||||
raise
|
||||
else:
|
||||
if valid:
|
||||
return r.user, r.access_token
|
||||
request.oauth2_error = getattr(r, "oauth2_error", {})
|
||||
return None
|
||||
|
||||
def authenticate_header(self, request):
|
||||
"""
|
||||
Bearer is the only finalized type currently
|
||||
"""
|
||||
www_authenticate_attributes = OrderedDict(
|
||||
[
|
||||
("realm", self.www_authenticate_realm),
|
||||
]
|
||||
)
|
||||
oauth2_error = getattr(request, "oauth2_error", {})
|
||||
www_authenticate_attributes.update(oauth2_error)
|
||||
return "Bearer {attributes}".format(
|
||||
attributes=self._dict_to_string(www_authenticate_attributes),
|
||||
)
|
||||
@@ -0,0 +1,183 @@
|
||||
import logging
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.permissions import SAFE_METHODS, BasePermission, IsAuthenticated
|
||||
|
||||
from ...settings import oauth2_settings
|
||||
from .authentication import OAuth2Authentication
|
||||
|
||||
|
||||
log = logging.getLogger("oauth2_provider")
|
||||
|
||||
|
||||
class TokenHasScope(BasePermission):
|
||||
"""
|
||||
The request is authenticated as a user and the token used has the right scope
|
||||
"""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
token = request.auth
|
||||
|
||||
if not token:
|
||||
return False
|
||||
|
||||
if hasattr(token, "scope"): # OAuth 2
|
||||
required_scopes = self.get_scopes(request, view)
|
||||
log.debug("Required scopes to access resource: {0}".format(required_scopes))
|
||||
|
||||
if token.is_valid(required_scopes):
|
||||
return True
|
||||
|
||||
# Provide information about required scope?
|
||||
include_required_scope = (
|
||||
oauth2_settings.ERROR_RESPONSE_WITH_SCOPES
|
||||
and required_scopes
|
||||
and not token.is_expired()
|
||||
and not token.allow_scopes(required_scopes)
|
||||
)
|
||||
|
||||
if include_required_scope:
|
||||
self.message = {
|
||||
"detail": PermissionDenied.default_detail,
|
||||
"required_scopes": list(required_scopes),
|
||||
}
|
||||
|
||||
return False
|
||||
|
||||
assert False, (
|
||||
"TokenHasScope requires the"
|
||||
"`oauth2_provider.rest_framework.OAuth2Authentication` authentication "
|
||||
"class to be used."
|
||||
)
|
||||
|
||||
def get_scopes(self, request, view):
|
||||
try:
|
||||
return getattr(view, "required_scopes")
|
||||
except AttributeError:
|
||||
raise ImproperlyConfigured(
|
||||
"TokenHasScope requires the view to define the required_scopes attribute"
|
||||
)
|
||||
|
||||
|
||||
class TokenHasReadWriteScope(TokenHasScope):
|
||||
"""
|
||||
The request is authenticated as a user and the token used has the right scope
|
||||
"""
|
||||
|
||||
def get_scopes(self, request, view):
|
||||
try:
|
||||
required_scopes = super().get_scopes(request, view)
|
||||
except ImproperlyConfigured:
|
||||
required_scopes = []
|
||||
|
||||
# TODO: code duplication!! see dispatch in ReadWriteScopedResourceMixin
|
||||
if request.method.upper() in SAFE_METHODS:
|
||||
read_write_scope = oauth2_settings.READ_SCOPE
|
||||
else:
|
||||
read_write_scope = oauth2_settings.WRITE_SCOPE
|
||||
|
||||
return required_scopes + [read_write_scope]
|
||||
|
||||
|
||||
class TokenHasResourceScope(TokenHasScope):
|
||||
"""
|
||||
The request is authenticated as a user and the token used has the right scope
|
||||
"""
|
||||
|
||||
def get_scopes(self, request, view):
|
||||
try:
|
||||
view_scopes = super().get_scopes(request, view)
|
||||
except ImproperlyConfigured:
|
||||
view_scopes = []
|
||||
|
||||
if request.method.upper() in SAFE_METHODS:
|
||||
scope_type = oauth2_settings.READ_SCOPE
|
||||
else:
|
||||
scope_type = oauth2_settings.WRITE_SCOPE
|
||||
|
||||
required_scopes = ["{}:{}".format(scope, scope_type) for scope in view_scopes]
|
||||
|
||||
return required_scopes
|
||||
|
||||
|
||||
class IsAuthenticatedOrTokenHasScope(BasePermission):
|
||||
"""
|
||||
The user is authenticated using some backend or the token has the right scope
|
||||
This only returns True if the user is authenticated, but not using a token
|
||||
or using a token, and the token has the correct scope.
|
||||
|
||||
This is useful when combined with the DjangoModelPermissions to allow people browse
|
||||
the browsable api's if they log in using the a non token bassed middleware,
|
||||
and let them access the api's using a rest client with a token
|
||||
"""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
is_authenticated = IsAuthenticated().has_permission(request, view)
|
||||
oauth2authenticated = False
|
||||
if is_authenticated:
|
||||
oauth2authenticated = isinstance(request.successful_authenticator, OAuth2Authentication)
|
||||
|
||||
token_has_scope = TokenHasScope()
|
||||
return (is_authenticated and not oauth2authenticated) or token_has_scope.has_permission(request, view)
|
||||
|
||||
|
||||
class TokenMatchesOASRequirements(BasePermission):
|
||||
"""
|
||||
:attr:alternate_required_scopes: dict keyed by HTTP method name with value: iterable alternate scope lists
|
||||
|
||||
This fulfills the [Open API Specification (OAS; formerly Swagger)](https://www.openapis.org/)
|
||||
list of alternative Security Requirements Objects for oauth2 or openIdConnect:
|
||||
When a list of Security Requirement Objects is defined on the Open API object or Operation Object,
|
||||
only one of Security Requirement Objects in the list needs to be satisfied to authorize the request.
|
||||
[1](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#securityRequirementObject)
|
||||
|
||||
For each method, a list of lists of allowed scopes is tried in order and the first to match succeeds.
|
||||
|
||||
@example
|
||||
required_alternate_scopes = {
|
||||
'GET': [['read']],
|
||||
'POST': [['create1','scope2'], ['alt-scope3'], ['alt-scope4','alt-scope5']],
|
||||
}
|
||||
|
||||
TODO: DRY: subclass TokenHasScope and iterate over values of required_scope?
|
||||
"""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
token = request.auth
|
||||
|
||||
if not token:
|
||||
return False
|
||||
|
||||
if hasattr(token, "scope"): # OAuth 2
|
||||
required_alternate_scopes = self.get_required_alternate_scopes(request, view)
|
||||
|
||||
m = request.method.upper()
|
||||
if m in required_alternate_scopes:
|
||||
log.debug(
|
||||
"Required scopes alternatives to access resource: {0}".format(
|
||||
required_alternate_scopes[m]
|
||||
)
|
||||
)
|
||||
for alt in required_alternate_scopes[m]:
|
||||
if token.is_valid(alt):
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
log.warning("no scope alternates defined for method {0}".format(m))
|
||||
return False
|
||||
|
||||
assert False, (
|
||||
"TokenMatchesOASRequirements requires the"
|
||||
"`oauth2_provider.rest_framework.OAuth2Authentication` authentication "
|
||||
"class to be used."
|
||||
)
|
||||
|
||||
def get_required_alternate_scopes(self, request, view):
|
||||
try:
|
||||
return getattr(view, "required_alternate_scopes")
|
||||
except AttributeError:
|
||||
raise ImproperlyConfigured(
|
||||
"TokenMatchesOASRequirements requires the view to"
|
||||
" define the required_alternate_scopes attribute"
|
||||
)
|
||||
@@ -0,0 +1,87 @@
|
||||
from functools import wraps
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.http import HttpResponseForbidden
|
||||
from oauthlib.oauth2 import Server
|
||||
|
||||
from .oauth2_backends import OAuthLibCore
|
||||
from .oauth2_validators import OAuth2Validator
|
||||
from .scopes import get_scopes_backend
|
||||
from .settings import oauth2_settings
|
||||
|
||||
|
||||
def protected_resource(scopes=None, validator_cls=OAuth2Validator, server_cls=Server):
|
||||
"""
|
||||
Decorator to protect views by providing OAuth2 authentication out of the box,
|
||||
optionally with scope handling.
|
||||
|
||||
@protected_resource()
|
||||
def my_view(request):
|
||||
# An access token is required to get here...
|
||||
# ...
|
||||
pass
|
||||
"""
|
||||
_scopes = scopes or []
|
||||
|
||||
def decorator(view_func):
|
||||
@wraps(view_func)
|
||||
def _validate(request, *args, **kwargs):
|
||||
validator = validator_cls()
|
||||
core = OAuthLibCore(server_cls(validator))
|
||||
valid, oauthlib_req = core.verify_request(request, scopes=_scopes)
|
||||
if valid:
|
||||
request.resource_owner = oauthlib_req.user
|
||||
return view_func(request, *args, **kwargs)
|
||||
return HttpResponseForbidden()
|
||||
|
||||
return _validate
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def rw_protected_resource(scopes=None, validator_cls=OAuth2Validator, server_cls=Server):
|
||||
"""
|
||||
Decorator to protect views by providing OAuth2 authentication and read/write scopes
|
||||
out of the box.
|
||||
GET, HEAD, OPTIONS http methods require "read" scope. Otherwise "write" scope is required.
|
||||
|
||||
@rw_protected_resource()
|
||||
def my_view(request):
|
||||
# If this is a POST, you have to provide 'write' scope to get here...
|
||||
# ...
|
||||
pass
|
||||
|
||||
"""
|
||||
_scopes = scopes or []
|
||||
|
||||
def decorator(view_func):
|
||||
@wraps(view_func)
|
||||
def _validate(request, *args, **kwargs):
|
||||
# Check if provided scopes are acceptable
|
||||
provided_scopes = get_scopes_backend().get_all_scopes()
|
||||
read_write_scopes = [oauth2_settings.READ_SCOPE, oauth2_settings.WRITE_SCOPE]
|
||||
|
||||
if not set(read_write_scopes).issubset(set(provided_scopes)):
|
||||
raise ImproperlyConfigured(
|
||||
"rw_protected_resource decorator requires following scopes {0}"
|
||||
" to be in OAUTH2_PROVIDER['SCOPES'] list in settings".format(read_write_scopes)
|
||||
)
|
||||
|
||||
# Check if method is safe
|
||||
if request.method.upper() in ["GET", "HEAD", "OPTIONS"]:
|
||||
_scopes.append(oauth2_settings.READ_SCOPE)
|
||||
else:
|
||||
_scopes.append(oauth2_settings.WRITE_SCOPE)
|
||||
|
||||
# proceed with validation
|
||||
validator = validator_cls()
|
||||
core = OAuthLibCore(server_cls(validator))
|
||||
valid, oauthlib_req = core.verify_request(request, scopes=_scopes)
|
||||
if valid:
|
||||
request.resource_owner = oauthlib_req.user
|
||||
return view_func(request, *args, **kwargs)
|
||||
return HttpResponseForbidden()
|
||||
|
||||
return _validate
|
||||
|
||||
return decorator
|
||||
@@ -0,0 +1,65 @@
|
||||
class OAuthToolkitError(Exception):
|
||||
"""
|
||||
Base class for exceptions
|
||||
"""
|
||||
|
||||
def __init__(self, error=None, redirect_uri=None, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.oauthlib_error = error
|
||||
|
||||
if redirect_uri:
|
||||
self.oauthlib_error.redirect_uri = redirect_uri
|
||||
|
||||
|
||||
class FatalClientError(OAuthToolkitError):
|
||||
"""
|
||||
Class for critical errors
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class OIDCError(Exception):
|
||||
"""
|
||||
General class to derive from for all OIDC related errors.
|
||||
"""
|
||||
|
||||
status_code = 400
|
||||
error = None
|
||||
|
||||
def __init__(self, description=None):
|
||||
if description is not None:
|
||||
self.description = description
|
||||
|
||||
message = "({}) {}".format(self.error, self.description)
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class InvalidRequestFatalError(OIDCError):
|
||||
"""
|
||||
For fatal errors. These are requests with invalid parameter values, missing parameters or otherwise
|
||||
incorrect requests.
|
||||
"""
|
||||
|
||||
error = "invalid_request"
|
||||
|
||||
|
||||
class ClientIdMissmatch(InvalidRequestFatalError):
|
||||
description = "Mismatch between the Client ID of the ID Token and the Client ID that was provided."
|
||||
|
||||
|
||||
class InvalidOIDCClientError(InvalidRequestFatalError):
|
||||
description = "The client is unknown or no client has been included."
|
||||
|
||||
|
||||
class InvalidOIDCRedirectURIError(InvalidRequestFatalError):
|
||||
description = "Invalid post logout redirect URI."
|
||||
|
||||
|
||||
class InvalidIDTokenError(InvalidRequestFatalError):
|
||||
description = "The ID Token is expired, revoked, malformed, or otherwise invalid."
|
||||
|
||||
|
||||
class LogoutDenied(OIDCError):
|
||||
error = "logout_denied"
|
||||
description = "Logout has been refused by the user."
|
||||
28
venv/lib/python3.11/site-packages/oauth2_provider/forms.py
Normal file
28
venv/lib/python3.11/site-packages/oauth2_provider/forms.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from django import forms
|
||||
|
||||
|
||||
class AllowForm(forms.Form):
|
||||
allow = forms.BooleanField(required=False)
|
||||
redirect_uri = forms.CharField(widget=forms.HiddenInput())
|
||||
scope = forms.CharField(widget=forms.HiddenInput())
|
||||
nonce = forms.CharField(required=False, widget=forms.HiddenInput())
|
||||
client_id = forms.CharField(widget=forms.HiddenInput())
|
||||
state = forms.CharField(required=False, widget=forms.HiddenInput())
|
||||
response_type = forms.CharField(widget=forms.HiddenInput())
|
||||
code_challenge = forms.CharField(required=False, widget=forms.HiddenInput())
|
||||
code_challenge_method = forms.CharField(required=False, widget=forms.HiddenInput())
|
||||
claims = forms.CharField(required=False, widget=forms.HiddenInput())
|
||||
|
||||
|
||||
class ConfirmLogoutForm(forms.Form):
|
||||
allow = forms.BooleanField(required=False)
|
||||
id_token_hint = forms.CharField(required=False, widget=forms.HiddenInput())
|
||||
logout_hint = forms.CharField(required=False, widget=forms.HiddenInput())
|
||||
client_id = forms.CharField(required=False, widget=forms.HiddenInput())
|
||||
post_logout_redirect_uri = forms.CharField(required=False, widget=forms.HiddenInput())
|
||||
state = forms.CharField(required=False, widget=forms.HiddenInput())
|
||||
ui_locales = forms.CharField(required=False, widget=forms.HiddenInput())
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.request = kwargs.pop("request", None)
|
||||
super(ConfirmLogoutForm, self).__init__(*args, **kwargs)
|
||||
@@ -0,0 +1,45 @@
|
||||
from oauthlib.common import UNICODE_ASCII_CHARACTER_SET
|
||||
from oauthlib.common import generate_client_id as oauthlib_generate_client_id
|
||||
|
||||
from .settings import oauth2_settings
|
||||
|
||||
|
||||
class BaseHashGenerator:
|
||||
"""
|
||||
All generators should extend this class overriding `.hash()` method.
|
||||
"""
|
||||
|
||||
def hash(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class ClientIdGenerator(BaseHashGenerator):
|
||||
def hash(self):
|
||||
"""
|
||||
Generate a client_id for Basic Authentication scheme without colon char
|
||||
as in https://rfc-editor.org/rfc/rfc2617.html#section-2
|
||||
"""
|
||||
return oauthlib_generate_client_id(length=40, chars=UNICODE_ASCII_CHARACTER_SET)
|
||||
|
||||
|
||||
class ClientSecretGenerator(BaseHashGenerator):
|
||||
def hash(self):
|
||||
length = oauth2_settings.CLIENT_SECRET_GENERATOR_LENGTH
|
||||
chars = UNICODE_ASCII_CHARACTER_SET
|
||||
return oauthlib_generate_client_id(length=length, chars=chars)
|
||||
|
||||
|
||||
def generate_client_id():
|
||||
"""
|
||||
Generate a suitable client id
|
||||
"""
|
||||
client_id_generator = oauth2_settings.CLIENT_ID_GENERATOR_CLASS()
|
||||
return client_id_generator.hash()
|
||||
|
||||
|
||||
def generate_client_secret():
|
||||
"""
|
||||
Generate a suitable client secret
|
||||
"""
|
||||
client_secret_generator = oauth2_settings.CLIENT_SECRET_GENERATOR_CLASS()
|
||||
return client_secret_generator.hash()
|
||||
32
venv/lib/python3.11/site-packages/oauth2_provider/http.py
Normal file
32
venv/lib/python3.11/site-packages/oauth2_provider/http.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.core.exceptions import DisallowedRedirect
|
||||
from django.http import HttpResponse
|
||||
from django.utils.encoding import iri_to_uri
|
||||
|
||||
|
||||
class OAuth2ResponseRedirect(HttpResponse):
|
||||
"""
|
||||
An HTTP 302 redirect with an explicit list of allowed schemes.
|
||||
Works like django.http.HttpResponseRedirect but we customize it
|
||||
to give us more flexibility on allowed scheme validation.
|
||||
"""
|
||||
|
||||
status_code = 302
|
||||
|
||||
def __init__(self, redirect_to, allowed_schemes, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self["Location"] = iri_to_uri(redirect_to)
|
||||
self.allowed_schemes = allowed_schemes
|
||||
self.validate_redirect(redirect_to)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self["Location"]
|
||||
|
||||
def validate_redirect(self, redirect_to):
|
||||
parsed = urlparse(str(redirect_to))
|
||||
if not parsed.scheme:
|
||||
raise DisallowedRedirect("OAuth2 redirects require a URI scheme.")
|
||||
if parsed.scheme not in self.allowed_schemes:
|
||||
raise DisallowedRedirect("Redirect to scheme {!r} is not permitted".format(parsed.scheme))
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,10 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from ...models import clear_expired
|
||||
|
||||
|
||||
class Command(BaseCommand): # pragma: no cover
|
||||
help = "Can be run as a cronjob or directly to clean out expired tokens"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
clear_expired()
|
||||
@@ -0,0 +1,109 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from oauth2_provider.models import get_application_model
|
||||
|
||||
|
||||
Application = get_application_model()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Shortcut to create a new application in a programmatic way"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"client_type",
|
||||
type=str,
|
||||
help="The client type, one of: %s" % ", ".join([ctype[0] for ctype in Application.CLIENT_TYPES]),
|
||||
)
|
||||
parser.add_argument(
|
||||
"authorization_grant_type",
|
||||
type=str,
|
||||
help="The type of authorization grant to be used, one of: %s"
|
||||
% ", ".join([gtype[0] for gtype in Application.GRANT_TYPES]),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--client-id",
|
||||
type=str,
|
||||
help="The ID of the new application",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--user",
|
||||
type=str,
|
||||
help="The user the application belongs to",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--redirect-uris",
|
||||
type=str,
|
||||
help="The redirect URIs, this must be a space separated string e.g 'URI1 URI2'",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--post-logout-redirect-uris",
|
||||
type=str,
|
||||
help="The post logout redirect URIs, this must be a space separated string e.g 'URI1 URI2'",
|
||||
default="",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--client-secret",
|
||||
type=str,
|
||||
help="The secret for this application",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-hash-client-secret",
|
||||
dest="hash_client_secret",
|
||||
action="store_false",
|
||||
help="Don't hash the client secret",
|
||||
)
|
||||
parser.set_defaults(hash_client_secret=True)
|
||||
parser.add_argument(
|
||||
"--name",
|
||||
type=str,
|
||||
help="The name this application",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-authorization",
|
||||
action="store_true",
|
||||
help="If set, completely bypass the authorization form, even on the first use of the application",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--algorithm",
|
||||
type=str,
|
||||
help="The OIDC token signing algorithm for this application, one of: %s"
|
||||
% ", ".join([atype[0] for atype in Application.ALGORITHM_TYPES if atype[0]]),
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
# Extract all fields related to the application, this will work now and in the future
|
||||
# and also with custom application models.
|
||||
application_fields = [field.name for field in Application._meta.fields]
|
||||
application_data = {}
|
||||
for key, value in options.items():
|
||||
# Data in options must be cleaned because there are unneeded key-value like
|
||||
# verbosity and others. Also do not pass any None to the Application
|
||||
# instance so default values will be generated for those fields
|
||||
if key in application_fields and (isinstance(value, bool) or value):
|
||||
if key == "user":
|
||||
application_data.update({"user_id": value})
|
||||
else:
|
||||
application_data.update({key: value})
|
||||
|
||||
new_application = Application(**application_data)
|
||||
|
||||
try:
|
||||
new_application.full_clean()
|
||||
except ValidationError as exc:
|
||||
errors = "\n ".join(
|
||||
["- " + err_key + ": " + str(err_value) for err_key, err_value in exc.message_dict.items()]
|
||||
)
|
||||
self.stdout.write(self.style.ERROR("Please correct the following errors:\n %s" % errors))
|
||||
else:
|
||||
cleartext_secret = new_application.client_secret
|
||||
new_application.save()
|
||||
# Display the newly-created client_name or id.
|
||||
client_name_or_id = application_data.get("name", new_application.client_id)
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS("New application %s created successfully." % client_name_or_id)
|
||||
)
|
||||
# Print out the cleartext client_secret if it was autogenerated.
|
||||
if "client_secret" not in application_data:
|
||||
self.stdout.write(self.style.SUCCESS("client_secret: %s" % cleartext_secret))
|
||||
@@ -0,0 +1,65 @@
|
||||
import hashlib
|
||||
import logging
|
||||
|
||||
from django.contrib.auth import authenticate
|
||||
from django.utils.cache import patch_vary_headers
|
||||
|
||||
from oauth2_provider.models import get_access_token_model
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OAuth2TokenMiddleware:
|
||||
"""
|
||||
Middleware for OAuth2 user authentication
|
||||
|
||||
This middleware is able to work along with AuthenticationMiddleware and its behaviour depends
|
||||
on the order it's processed with.
|
||||
|
||||
If it comes *after* AuthenticationMiddleware and request.user is valid, leave it as is and does
|
||||
not proceed with token validation. If request.user is the Anonymous user proceeds and try to
|
||||
authenticate the user using the OAuth2 access token.
|
||||
|
||||
If it comes *before* AuthenticationMiddleware, or AuthenticationMiddleware is not used at all,
|
||||
tries to authenticate user with the OAuth2 access token and set request.user field. Setting
|
||||
also request._cached_user field makes AuthenticationMiddleware use that instead of the one from
|
||||
the session.
|
||||
|
||||
It also adds "Authorization" to the "Vary" header, so that django's cache middleware or a
|
||||
reverse proxy can create proper cache keys.
|
||||
"""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
# do something only if request contains a Bearer token
|
||||
if request.META.get("HTTP_AUTHORIZATION", "").startswith("Bearer"):
|
||||
if not hasattr(request, "user") or request.user.is_anonymous:
|
||||
user = authenticate(request=request)
|
||||
if user:
|
||||
request.user = request._cached_user = user
|
||||
|
||||
response = self.get_response(request)
|
||||
patch_vary_headers(response, ("Authorization",))
|
||||
return response
|
||||
|
||||
|
||||
class OAuth2ExtraTokenMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
authheader = request.META.get("HTTP_AUTHORIZATION", "")
|
||||
if authheader.startswith("Bearer"):
|
||||
tokenstring = authheader.split()[1]
|
||||
AccessToken = get_access_token_model()
|
||||
try:
|
||||
token_checksum = hashlib.sha256(tokenstring.encode("utf-8")).hexdigest()
|
||||
token = AccessToken.objects.get(token_checksum=token_checksum)
|
||||
request.access_token = token
|
||||
except AccessToken.DoesNotExist as e:
|
||||
log.exception(e)
|
||||
response = self.get_response(request)
|
||||
return response
|
||||
@@ -0,0 +1,105 @@
|
||||
from django.conf import settings
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
import oauth2_provider.generators
|
||||
import oauth2_provider.validators
|
||||
from oauth2_provider.settings import oauth2_settings
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
"""
|
||||
The following migrations are squashed here:
|
||||
- 0001_initial.py
|
||||
- 0002_08_updates.py
|
||||
- 0003_auto_20160316_1503.py
|
||||
- 0004_auto_20160525_1623.py
|
||||
- 0005_auto_20170514_1141.py
|
||||
- 0006_auto_20171214_2232.py
|
||||
"""
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL)
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Application',
|
||||
fields=[
|
||||
('id', models.BigAutoField(serialize=False, primary_key=True)),
|
||||
('client_id', models.CharField(default=oauth2_provider.generators.generate_client_id, unique=True, max_length=100, db_index=True)),
|
||||
('redirect_uris', models.TextField(help_text='Allowed URIs list, space separated', blank=True)),
|
||||
('client_type', models.CharField(max_length=32, choices=[('confidential', 'Confidential'), ('public', 'Public')])),
|
||||
('authorization_grant_type', models.CharField(max_length=32, choices=[('authorization-code', 'Authorization code'), ('implicit', 'Implicit'), ('password', 'Resource owner password-based'), ('client-credentials', 'Client credentials')])),
|
||||
('client_secret', models.CharField(default=oauth2_provider.generators.generate_client_secret, max_length=255, db_index=True, blank=True)),
|
||||
('name', models.CharField(max_length=255, blank=True)),
|
||||
('user', models.ForeignKey(related_name="oauth2_provider_application", blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE)),
|
||||
('skip_authorization', models.BooleanField(default=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('updated', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
'swappable': 'OAUTH2_PROVIDER_APPLICATION_MODEL',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AccessToken',
|
||||
fields=[
|
||||
('id', models.BigAutoField(serialize=False, primary_key=True)),
|
||||
('token', models.CharField(unique=True, max_length=255)),
|
||||
('expires', models.DateTimeField()),
|
||||
('scope', models.TextField(blank=True)),
|
||||
('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=oauth2_settings.APPLICATION_MODEL)),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='oauth2_provider_accesstoken', to=settings.AUTH_USER_MODEL)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('updated', models.DateTimeField(auto_now=True)),
|
||||
# Circular reference. Can't add it here.
|
||||
#('source_refresh_token', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=oauth2_settings.REFRESH_TOKEN_MODEL, related_name="refreshed_access_token")),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
'swappable': 'OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Grant',
|
||||
fields=[
|
||||
('id', models.BigAutoField(serialize=False, primary_key=True)),
|
||||
('code', models.CharField(unique=True, max_length=255)),
|
||||
('expires', models.DateTimeField()),
|
||||
('redirect_uri', models.CharField(max_length=255)),
|
||||
('scope', models.TextField(blank=True)),
|
||||
('application', models.ForeignKey(to=oauth2_settings.APPLICATION_MODEL, on_delete=models.CASCADE)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='oauth2_provider_grant', to=settings.AUTH_USER_MODEL)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('updated', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
'swappable': 'OAUTH2_PROVIDER_GRANT_MODEL',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RefreshToken',
|
||||
fields=[
|
||||
('id', models.BigAutoField(serialize=False, primary_key=True)),
|
||||
('token', models.CharField(max_length=255)),
|
||||
('access_token', models.OneToOneField(blank=True, null=True, related_name="refresh_token", to=oauth2_settings.ACCESS_TOKEN_MODEL, on_delete=models.SET_NULL)),
|
||||
('application', models.ForeignKey(to=oauth2_settings.APPLICATION_MODEL, on_delete=models.CASCADE)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='oauth2_provider_refreshtoken', to=settings.AUTH_USER_MODEL)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('updated', models.DateTimeField(auto_now=True)),
|
||||
('revoked', models.DateTimeField(null=True)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
'swappable': 'OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL',
|
||||
'unique_together': set([("token", "revoked")]),
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='AccessToken',
|
||||
name='source_refresh_token',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=oauth2_settings.REFRESH_TOKEN_MODEL, related_name="refreshed_access_token"),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 2.2 on 2019-04-06 18:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('oauth2_provider', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='grant',
|
||||
name='code_challenge',
|
||||
field=models.CharField(blank=True, default='', max_length=128),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='grant',
|
||||
name='code_challenge_method',
|
||||
field=models.CharField(blank=True, choices=[('plain', 'plain'), ('S256', 'S256')], default='', max_length=10),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.4 on 2020-12-11 13:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('oauth2_provider', '0002_auto_20190406_1805'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='grant',
|
||||
name='redirect_uri',
|
||||
field=models.TextField(),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,60 @@
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
from oauth2_provider.settings import oauth2_settings
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('oauth2_provider', '0003_auto_20201211_1314'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='application',
|
||||
name='algorithm',
|
||||
field=models.CharField(blank=True, choices=[("", "No OIDC support"), ('RS256', 'RSA with SHA-2 256'), ('HS256', 'HMAC with SHA-2 256')], default='', max_length=5),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='application',
|
||||
name='authorization_grant_type',
|
||||
field=models.CharField(choices=[('authorization-code', 'Authorization code'), ('implicit', 'Implicit'), ('password', 'Resource owner password-based'), ('client-credentials', 'Client credentials'), ('openid-hybrid', 'OpenID connect hybrid')], max_length=32),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='IDToken',
|
||||
fields=[
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
("jti", models.UUIDField(unique=True, default=uuid.uuid4, editable=False, verbose_name="JWT Token ID")),
|
||||
('expires', models.DateTimeField()),
|
||||
('scope', models.TextField(blank=True)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('updated', models.DateTimeField(auto_now=True)),
|
||||
('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=oauth2_settings.APPLICATION_MODEL)),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='oauth2_provider_idtoken', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
'swappable': 'OAUTH2_PROVIDER_ID_TOKEN_MODEL',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='accesstoken',
|
||||
name='id_token',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='access_token', to=oauth2_settings.ID_TOKEN_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="grant",
|
||||
name="nonce",
|
||||
field=models.CharField(blank=True, max_length=255, default=""),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="grant",
|
||||
name="claims",
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,39 @@
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('oauth2_provider', '0004_auto_20200902_2022'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='accesstoken',
|
||||
name='user',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='application',
|
||||
name='user',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='grant',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='idtoken',
|
||||
name='user',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='refreshtoken',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,43 @@
|
||||
import logging
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
import oauth2_provider.generators
|
||||
import oauth2_provider.models
|
||||
from oauth2_provider import settings
|
||||
|
||||
|
||||
logger = logging.getLogger()
|
||||
|
||||
|
||||
def forwards_func(apps, schema_editor):
|
||||
"""
|
||||
Forward migration touches every application.client_secret which will cause it to be hashed if not already the case.
|
||||
"""
|
||||
Application = apps.get_model(settings.APPLICATION_MODEL)
|
||||
applications = Application._default_manager.all()
|
||||
for application in applications:
|
||||
application.save(update_fields=['client_secret'])
|
||||
|
||||
|
||||
def reverse_func(apps, schema_editor):
|
||||
warning_color_code = "\033[93m"
|
||||
end_color_code = "\033[0m"
|
||||
msg = f"\n{warning_color_code}The previously hashed client_secret cannot be reverted, and it remains hashed{end_color_code}"
|
||||
logger.warning(msg)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('oauth2_provider', '0005_auto_20211222_2352'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='application',
|
||||
name='client_secret',
|
||||
field=oauth2_provider.models.ClientSecretField(blank=True, db_index=True, default=oauth2_provider.generators.generate_client_secret, help_text='Hashed on Save. Copy it now if this is a new secret.', max_length=255),
|
||||
),
|
||||
migrations.RunPython(forwards_func, reverse_func),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.1.5 on 2023-01-14 12:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("oauth2_provider", "0006_alter_application_client_secret"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="application",
|
||||
name="post_logout_redirect_uris",
|
||||
field=models.TextField(blank=True, help_text="Allowed Post Logout URIs list, space separated", default=""),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.2.4 on 2023-09-11 07:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("oauth2_provider", "0007_application_post_logout_redirect_uris"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="accesstoken",
|
||||
name="token",
|
||||
field=models.CharField(db_index=True, max_length=255, unique=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.5 on 2023-09-07 19:26
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('oauth2_provider', '0008_alter_accesstoken_token'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='application',
|
||||
name='hash_client_secret',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 4.1.5 on 2023-09-27 20:15
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("oauth2_provider", "0009_add_hash_client_secret"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="application",
|
||||
name="allowed_origins",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
help_text="Allowed origins list to enable CORS, space separated",
|
||||
default="",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.2 on 2024-08-09 16:40
|
||||
|
||||
from django.db import migrations, models
|
||||
from oauth2_provider.settings import oauth2_settings
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('oauth2_provider', '0010_application_allowed_origins'),
|
||||
migrations.swappable_dependency(oauth2_settings.REFRESH_TOKEN_MODEL)
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='refreshtoken',
|
||||
name='token_family',
|
||||
field=models.UUIDField(blank=True, editable=False, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,40 @@
|
||||
# Generated by Django 5.0.7 on 2024-07-29 23:13
|
||||
|
||||
import oauth2_provider.models
|
||||
from django.db import migrations, models
|
||||
from oauth2_provider.settings import oauth2_settings
|
||||
|
||||
def forwards_func(apps, schema_editor):
|
||||
"""
|
||||
Forward migration touches every "old" accesstoken.token which will cause the checksum to be computed.
|
||||
"""
|
||||
AccessToken = apps.get_model(oauth2_settings.ACCESS_TOKEN_MODEL)
|
||||
accesstokens = AccessToken._default_manager.all()
|
||||
for accesstoken in accesstokens:
|
||||
accesstoken.save(update_fields=['token_checksum'])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("oauth2_provider", "0011_refreshtoken_token_family"),
|
||||
migrations.swappable_dependency(oauth2_settings.ACCESS_TOKEN_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="accesstoken",
|
||||
name="token_checksum",
|
||||
field=oauth2_provider.models.TokenChecksumField(blank=True, null=True, max_length=64),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="accesstoken",
|
||||
name="token",
|
||||
field=models.TextField(),
|
||||
),
|
||||
migrations.RunPython(forwards_func, migrations.RunPython.noop),
|
||||
migrations.AlterField(
|
||||
model_name='accesstoken',
|
||||
name='token_checksum',
|
||||
field=oauth2_provider.models.TokenChecksumField(blank=False, max_length=64, db_index=True, unique=True),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
836
venv/lib/python3.11/site-packages/oauth2_provider/models.py
Normal file
836
venv/lib/python3.11/site-packages/oauth2_provider/models.py
Normal file
@@ -0,0 +1,836 @@
|
||||
import hashlib
|
||||
import logging
|
||||
import time
|
||||
import uuid
|
||||
from contextlib import suppress
|
||||
from datetime import timedelta
|
||||
from urllib.parse import parse_qsl, urlparse
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.hashers import identify_hasher, make_password
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db import models, router, transaction
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from jwcrypto import jwk
|
||||
from jwcrypto.common import base64url_encode
|
||||
from oauthlib.oauth2.rfc6749 import errors
|
||||
|
||||
from .generators import generate_client_id, generate_client_secret
|
||||
from .scopes import get_scopes_backend
|
||||
from .settings import oauth2_settings
|
||||
from .utils import jwk_from_pem
|
||||
from .validators import AllowedURIValidator
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ClientSecretField(models.CharField):
|
||||
def pre_save(self, model_instance, add):
|
||||
secret = getattr(model_instance, self.attname)
|
||||
should_be_hashed = getattr(model_instance, "hash_client_secret", True)
|
||||
if not should_be_hashed:
|
||||
return super().pre_save(model_instance, add)
|
||||
|
||||
try:
|
||||
hasher = identify_hasher(secret)
|
||||
logger.debug(f"{model_instance}: {self.attname} is already hashed with {hasher}.")
|
||||
except ValueError:
|
||||
logger.debug(f"{model_instance}: {self.attname} is not hashed; hashing it now.")
|
||||
hashed_secret = make_password(secret)
|
||||
setattr(model_instance, self.attname, hashed_secret)
|
||||
return hashed_secret
|
||||
return super().pre_save(model_instance, add)
|
||||
|
||||
|
||||
class TokenChecksumField(models.CharField):
|
||||
def pre_save(self, model_instance, add):
|
||||
token = getattr(model_instance, "token")
|
||||
checksum = hashlib.sha256(token.encode("utf-8")).hexdigest()
|
||||
setattr(model_instance, self.attname, checksum)
|
||||
return super().pre_save(model_instance, add)
|
||||
|
||||
|
||||
class AbstractApplication(models.Model):
|
||||
"""
|
||||
An Application instance represents a Client on the Authorization server.
|
||||
Usually an Application is created manually by client's developers after
|
||||
logging in on an Authorization Server.
|
||||
|
||||
Fields:
|
||||
|
||||
* :attr:`client_id` The client identifier issued to the client during the
|
||||
registration process as described in :rfc:`2.2`
|
||||
* :attr:`user` ref to a Django user
|
||||
* :attr:`redirect_uris` The list of allowed redirect uri. The string
|
||||
consists of valid URLs separated by space
|
||||
* :attr:`post_logout_redirect_uris` The list of allowed redirect uris after
|
||||
an RP initiated logout. The string
|
||||
consists of valid URLs separated by space
|
||||
* :attr:`client_type` Client type as described in :rfc:`2.1`
|
||||
* :attr:`authorization_grant_type` Authorization flows available to the
|
||||
Application
|
||||
* :attr:`client_secret` Confidential secret issued to the client during
|
||||
the registration process as described in :rfc:`2.2`
|
||||
* :attr:`name` Friendly name for the Application
|
||||
"""
|
||||
|
||||
CLIENT_CONFIDENTIAL = "confidential"
|
||||
CLIENT_PUBLIC = "public"
|
||||
CLIENT_TYPES = (
|
||||
(CLIENT_CONFIDENTIAL, _("Confidential")),
|
||||
(CLIENT_PUBLIC, _("Public")),
|
||||
)
|
||||
|
||||
GRANT_AUTHORIZATION_CODE = "authorization-code"
|
||||
GRANT_IMPLICIT = "implicit"
|
||||
GRANT_PASSWORD = "password"
|
||||
GRANT_CLIENT_CREDENTIALS = "client-credentials"
|
||||
GRANT_OPENID_HYBRID = "openid-hybrid"
|
||||
GRANT_TYPES = (
|
||||
(GRANT_AUTHORIZATION_CODE, _("Authorization code")),
|
||||
(GRANT_IMPLICIT, _("Implicit")),
|
||||
(GRANT_PASSWORD, _("Resource owner password-based")),
|
||||
(GRANT_CLIENT_CREDENTIALS, _("Client credentials")),
|
||||
(GRANT_OPENID_HYBRID, _("OpenID connect hybrid")),
|
||||
)
|
||||
|
||||
NO_ALGORITHM = ""
|
||||
RS256_ALGORITHM = "RS256"
|
||||
HS256_ALGORITHM = "HS256"
|
||||
ALGORITHM_TYPES = (
|
||||
(NO_ALGORITHM, _("No OIDC support")),
|
||||
(RS256_ALGORITHM, _("RSA with SHA-2 256")),
|
||||
(HS256_ALGORITHM, _("HMAC with SHA-2 256")),
|
||||
)
|
||||
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
client_id = models.CharField(max_length=100, unique=True, default=generate_client_id, db_index=True)
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
related_name="%(app_label)s_%(class)s",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
redirect_uris = models.TextField(
|
||||
blank=True,
|
||||
help_text=_("Allowed URIs list, space separated"),
|
||||
)
|
||||
post_logout_redirect_uris = models.TextField(
|
||||
blank=True,
|
||||
help_text=_("Allowed Post Logout URIs list, space separated"),
|
||||
default="",
|
||||
)
|
||||
client_type = models.CharField(max_length=32, choices=CLIENT_TYPES)
|
||||
authorization_grant_type = models.CharField(max_length=32, choices=GRANT_TYPES)
|
||||
client_secret = ClientSecretField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
default=generate_client_secret,
|
||||
db_index=True,
|
||||
help_text=_("Hashed on Save. Copy it now if this is a new secret."),
|
||||
)
|
||||
hash_client_secret = models.BooleanField(default=True)
|
||||
name = models.CharField(max_length=255, blank=True)
|
||||
skip_authorization = models.BooleanField(default=False)
|
||||
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
updated = models.DateTimeField(auto_now=True)
|
||||
algorithm = models.CharField(max_length=5, choices=ALGORITHM_TYPES, default=NO_ALGORITHM, blank=True)
|
||||
allowed_origins = models.TextField(
|
||||
blank=True,
|
||||
help_text=_("Allowed origins list to enable CORS, space separated"),
|
||||
default="",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def __str__(self):
|
||||
return self.name or self.client_id
|
||||
|
||||
@property
|
||||
def default_redirect_uri(self):
|
||||
"""
|
||||
Returns the default redirect_uri, *if* only one is registered.
|
||||
"""
|
||||
if self.redirect_uris:
|
||||
uris = self.redirect_uris.split()
|
||||
if len(uris) == 1:
|
||||
return self.redirect_uris.split().pop(0)
|
||||
raise errors.MissingRedirectURIError()
|
||||
|
||||
assert False, (
|
||||
"If you are using implicit, authorization_code "
|
||||
"or all-in-one grant_type, you must define "
|
||||
"redirect_uris field in your Application model"
|
||||
)
|
||||
|
||||
def redirect_uri_allowed(self, uri):
|
||||
"""
|
||||
Checks if given url is one of the items in :attr:`redirect_uris` string
|
||||
|
||||
:param uri: Url to check
|
||||
"""
|
||||
return redirect_to_uri_allowed(uri, self.redirect_uris.split())
|
||||
|
||||
def post_logout_redirect_uri_allowed(self, uri):
|
||||
"""
|
||||
Checks if given URI is one of the items in :attr:`post_logout_redirect_uris` string
|
||||
|
||||
:param uri: URI to check
|
||||
"""
|
||||
return redirect_to_uri_allowed(uri, self.post_logout_redirect_uris.split())
|
||||
|
||||
def origin_allowed(self, origin):
|
||||
"""
|
||||
Checks if given origin is one of the items in :attr:`allowed_origins` string
|
||||
|
||||
:param origin: Origin to check
|
||||
"""
|
||||
return self.allowed_origins and is_origin_allowed(origin, self.allowed_origins.split())
|
||||
|
||||
def clean(self):
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
grant_types = (
|
||||
AbstractApplication.GRANT_AUTHORIZATION_CODE,
|
||||
AbstractApplication.GRANT_IMPLICIT,
|
||||
AbstractApplication.GRANT_OPENID_HYBRID,
|
||||
)
|
||||
hs_forbidden_grant_types = (
|
||||
AbstractApplication.GRANT_IMPLICIT,
|
||||
AbstractApplication.GRANT_OPENID_HYBRID,
|
||||
)
|
||||
|
||||
redirect_uris = self.redirect_uris.strip().split()
|
||||
allowed_schemes = set(s.lower() for s in self.get_allowed_schemes())
|
||||
|
||||
if redirect_uris:
|
||||
validator = AllowedURIValidator(
|
||||
allowed_schemes, name="redirect uri", allow_path=True, allow_query=True
|
||||
)
|
||||
for uri in redirect_uris:
|
||||
validator(uri)
|
||||
|
||||
elif self.authorization_grant_type in grant_types:
|
||||
raise ValidationError(
|
||||
_("redirect_uris cannot be empty with grant_type {grant_type}").format(
|
||||
grant_type=self.authorization_grant_type
|
||||
)
|
||||
)
|
||||
allowed_origins = self.allowed_origins.strip().split()
|
||||
if allowed_origins:
|
||||
# oauthlib allows only https scheme for CORS
|
||||
validator = AllowedURIValidator(oauth2_settings.ALLOWED_SCHEMES, "allowed origin")
|
||||
for uri in allowed_origins:
|
||||
validator(uri)
|
||||
|
||||
if self.algorithm == AbstractApplication.RS256_ALGORITHM:
|
||||
if not oauth2_settings.OIDC_RSA_PRIVATE_KEY:
|
||||
raise ValidationError(_("You must set OIDC_RSA_PRIVATE_KEY to use RSA algorithm"))
|
||||
|
||||
if self.algorithm == AbstractApplication.HS256_ALGORITHM:
|
||||
if any(
|
||||
(
|
||||
self.authorization_grant_type in hs_forbidden_grant_types,
|
||||
self.client_type == Application.CLIENT_PUBLIC,
|
||||
)
|
||||
):
|
||||
raise ValidationError(_("You cannot use HS256 with public grants or clients"))
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("oauth2_provider:detail", args=[str(self.pk)])
|
||||
|
||||
def get_allowed_schemes(self):
|
||||
"""
|
||||
Returns the list of redirect schemes allowed by the Application.
|
||||
By default, returns `ALLOWED_REDIRECT_URI_SCHEMES`.
|
||||
"""
|
||||
return oauth2_settings.ALLOWED_REDIRECT_URI_SCHEMES
|
||||
|
||||
def allows_grant_type(self, *grant_types):
|
||||
return self.authorization_grant_type in grant_types
|
||||
|
||||
def is_usable(self, request):
|
||||
"""
|
||||
Determines whether the application can be used.
|
||||
|
||||
:param request: The oauthlib.common.Request being processed.
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def jwk_key(self):
|
||||
if self.algorithm == AbstractApplication.RS256_ALGORITHM:
|
||||
if not oauth2_settings.OIDC_RSA_PRIVATE_KEY:
|
||||
raise ImproperlyConfigured("You must set OIDC_RSA_PRIVATE_KEY to use RSA algorithm")
|
||||
return jwk_from_pem(oauth2_settings.OIDC_RSA_PRIVATE_KEY)
|
||||
elif self.algorithm == AbstractApplication.HS256_ALGORITHM:
|
||||
return jwk.JWK(kty="oct", k=base64url_encode(self.client_secret))
|
||||
raise ImproperlyConfigured("This application does not support signed tokens")
|
||||
|
||||
|
||||
class ApplicationManager(models.Manager):
|
||||
def get_by_natural_key(self, client_id):
|
||||
return self.get(client_id=client_id)
|
||||
|
||||
|
||||
class Application(AbstractApplication):
|
||||
objects = ApplicationManager()
|
||||
|
||||
class Meta(AbstractApplication.Meta):
|
||||
swappable = "OAUTH2_PROVIDER_APPLICATION_MODEL"
|
||||
|
||||
def natural_key(self):
|
||||
return (self.client_id,)
|
||||
|
||||
|
||||
class AbstractGrant(models.Model):
|
||||
"""
|
||||
A Grant instance represents a token with a short lifetime that can
|
||||
be swapped for an access token, as described in :rfc:`4.1.2`
|
||||
|
||||
Fields:
|
||||
|
||||
* :attr:`user` The Django user who requested the grant
|
||||
* :attr:`code` The authorization code generated by the authorization server
|
||||
* :attr:`application` Application instance this grant was asked for
|
||||
* :attr:`expires` Expire time in seconds, defaults to
|
||||
:data:`settings.AUTHORIZATION_CODE_EXPIRE_SECONDS`
|
||||
* :attr:`redirect_uri` Self explained
|
||||
* :attr:`scope` Required scopes, optional
|
||||
* :attr:`code_challenge` PKCE code challenge
|
||||
* :attr:`code_challenge_method` PKCE code challenge transform algorithm
|
||||
"""
|
||||
|
||||
CODE_CHALLENGE_PLAIN = "plain"
|
||||
CODE_CHALLENGE_S256 = "S256"
|
||||
CODE_CHALLENGE_METHODS = ((CODE_CHALLENGE_PLAIN, "plain"), (CODE_CHALLENGE_S256, "S256"))
|
||||
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="%(app_label)s_%(class)s"
|
||||
)
|
||||
code = models.CharField(max_length=255, unique=True) # code comes from oauthlib
|
||||
application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.CASCADE)
|
||||
expires = models.DateTimeField()
|
||||
redirect_uri = models.TextField()
|
||||
scope = models.TextField(blank=True)
|
||||
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
updated = models.DateTimeField(auto_now=True)
|
||||
|
||||
code_challenge = models.CharField(max_length=128, blank=True, default="")
|
||||
code_challenge_method = models.CharField(
|
||||
max_length=10, blank=True, default="", choices=CODE_CHALLENGE_METHODS
|
||||
)
|
||||
|
||||
nonce = models.CharField(max_length=255, blank=True, default="")
|
||||
claims = models.TextField(blank=True)
|
||||
|
||||
def is_expired(self):
|
||||
"""
|
||||
Check token expiration with timezone awareness
|
||||
"""
|
||||
if not self.expires:
|
||||
return True
|
||||
|
||||
return timezone.now() >= self.expires
|
||||
|
||||
def redirect_uri_allowed(self, uri):
|
||||
return uri == self.redirect_uri
|
||||
|
||||
def __str__(self):
|
||||
return self.code
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class Grant(AbstractGrant):
|
||||
class Meta(AbstractGrant.Meta):
|
||||
swappable = "OAUTH2_PROVIDER_GRANT_MODEL"
|
||||
|
||||
|
||||
class AbstractAccessToken(models.Model):
|
||||
"""
|
||||
An AccessToken instance represents the actual access token to
|
||||
access user's resources, as in :rfc:`5`.
|
||||
|
||||
Fields:
|
||||
|
||||
* :attr:`user` The Django user representing resources" owner
|
||||
* :attr:`source_refresh_token` If from a refresh, the consumed RefeshToken
|
||||
* :attr:`token` Access token
|
||||
* :attr:`application` Application instance
|
||||
* :attr:`expires` Date and time of token expiration, in DateTime format
|
||||
* :attr:`scope` Allowed scopes
|
||||
"""
|
||||
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
blank=True,
|
||||
null=True,
|
||||
related_name="%(app_label)s_%(class)s",
|
||||
)
|
||||
source_refresh_token = models.OneToOneField(
|
||||
# unique=True implied by the OneToOneField
|
||||
oauth2_settings.REFRESH_TOKEN_MODEL,
|
||||
on_delete=models.SET_NULL,
|
||||
blank=True,
|
||||
null=True,
|
||||
related_name="refreshed_access_token",
|
||||
)
|
||||
token = models.TextField()
|
||||
token_checksum = TokenChecksumField(
|
||||
max_length=64,
|
||||
blank=False,
|
||||
unique=True,
|
||||
db_index=True,
|
||||
)
|
||||
id_token = models.OneToOneField(
|
||||
oauth2_settings.ID_TOKEN_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
blank=True,
|
||||
null=True,
|
||||
related_name="access_token",
|
||||
)
|
||||
application = models.ForeignKey(
|
||||
oauth2_settings.APPLICATION_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
expires = models.DateTimeField()
|
||||
scope = models.TextField(blank=True)
|
||||
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
updated = models.DateTimeField(auto_now=True)
|
||||
|
||||
def is_valid(self, scopes=None):
|
||||
"""
|
||||
Checks if the access token is valid.
|
||||
|
||||
:param scopes: An iterable containing the scopes to check or None
|
||||
"""
|
||||
return not self.is_expired() and self.allow_scopes(scopes)
|
||||
|
||||
def is_expired(self):
|
||||
"""
|
||||
Check token expiration with timezone awareness
|
||||
"""
|
||||
if not self.expires:
|
||||
return True
|
||||
|
||||
return timezone.now() >= self.expires
|
||||
|
||||
def allow_scopes(self, scopes):
|
||||
"""
|
||||
Check if the token allows the provided scopes
|
||||
|
||||
:param scopes: An iterable containing the scopes to check
|
||||
"""
|
||||
if not scopes:
|
||||
return True
|
||||
|
||||
provided_scopes = set(self.scope.split())
|
||||
resource_scopes = set(scopes)
|
||||
|
||||
return resource_scopes.issubset(provided_scopes)
|
||||
|
||||
def revoke(self):
|
||||
"""
|
||||
Convenience method to uniform tokens" interface, for now
|
||||
simply remove this token from the database in order to revoke it.
|
||||
"""
|
||||
self.delete()
|
||||
|
||||
@property
|
||||
def scopes(self):
|
||||
"""
|
||||
Returns a dictionary of allowed scope names (as keys) with their descriptions (as values)
|
||||
"""
|
||||
all_scopes = get_scopes_backend().get_all_scopes()
|
||||
token_scopes = self.scope.split()
|
||||
return {name: desc for name, desc in all_scopes.items() if name in token_scopes}
|
||||
|
||||
def __str__(self):
|
||||
return self.token
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class AccessToken(AbstractAccessToken):
|
||||
class Meta(AbstractAccessToken.Meta):
|
||||
swappable = "OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL"
|
||||
|
||||
|
||||
class AbstractRefreshToken(models.Model):
|
||||
"""
|
||||
A RefreshToken instance represents a token that can be swapped for a new
|
||||
access token when it expires.
|
||||
|
||||
Fields:
|
||||
|
||||
* :attr:`user` The Django user representing resources" owner
|
||||
* :attr:`token` Token value
|
||||
* :attr:`application` Application instance
|
||||
* :attr:`access_token` AccessToken instance this refresh token is
|
||||
bounded to
|
||||
* :attr:`revoked` Timestamp of when this refresh token was revoked
|
||||
"""
|
||||
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="%(app_label)s_%(class)s"
|
||||
)
|
||||
token = models.CharField(max_length=255)
|
||||
application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.CASCADE)
|
||||
access_token = models.OneToOneField(
|
||||
oauth2_settings.ACCESS_TOKEN_MODEL,
|
||||
on_delete=models.SET_NULL,
|
||||
blank=True,
|
||||
null=True,
|
||||
related_name="refresh_token",
|
||||
)
|
||||
token_family = models.UUIDField(null=True, blank=True, editable=False)
|
||||
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
updated = models.DateTimeField(auto_now=True)
|
||||
revoked = models.DateTimeField(null=True)
|
||||
|
||||
def revoke(self):
|
||||
"""
|
||||
Mark this refresh token revoked and revoke related access token
|
||||
"""
|
||||
access_token_model = get_access_token_model()
|
||||
access_token_database = router.db_for_write(access_token_model)
|
||||
refresh_token_model = get_refresh_token_model()
|
||||
|
||||
# Use the access_token_database instead of making the assumption it is in 'default'.
|
||||
with transaction.atomic(using=access_token_database):
|
||||
token = refresh_token_model.objects.select_for_update().filter(pk=self.pk, revoked__isnull=True)
|
||||
if not token:
|
||||
return
|
||||
self = list(token)[0]
|
||||
|
||||
with suppress(access_token_model.DoesNotExist):
|
||||
access_token_model.objects.get(id=self.access_token_id).revoke()
|
||||
|
||||
self.access_token = None
|
||||
self.revoked = timezone.now()
|
||||
self.save()
|
||||
|
||||
def __str__(self):
|
||||
return self.token
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
unique_together = (
|
||||
"token",
|
||||
"revoked",
|
||||
)
|
||||
|
||||
|
||||
class RefreshToken(AbstractRefreshToken):
|
||||
class Meta(AbstractRefreshToken.Meta):
|
||||
swappable = "OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL"
|
||||
|
||||
|
||||
class AbstractIDToken(models.Model):
|
||||
"""
|
||||
An IDToken instance represents the actual token to
|
||||
access user's resources, as in :openid:`2`.
|
||||
|
||||
Fields:
|
||||
|
||||
* :attr:`user` The Django user representing resources' owner
|
||||
* :attr:`jti` ID token JWT Token ID, to identify an individual token
|
||||
* :attr:`application` Application instance
|
||||
* :attr:`expires` Date and time of token expiration, in DateTime format
|
||||
* :attr:`scope` Allowed scopes
|
||||
* :attr:`created` Date and time of token creation, in DateTime format
|
||||
* :attr:`updated` Date and time of token update, in DateTime format
|
||||
"""
|
||||
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
blank=True,
|
||||
null=True,
|
||||
related_name="%(app_label)s_%(class)s",
|
||||
)
|
||||
jti = models.UUIDField(unique=True, default=uuid.uuid4, editable=False, verbose_name="JWT Token ID")
|
||||
application = models.ForeignKey(
|
||||
oauth2_settings.APPLICATION_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
expires = models.DateTimeField()
|
||||
scope = models.TextField(blank=True)
|
||||
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
updated = models.DateTimeField(auto_now=True)
|
||||
|
||||
def is_valid(self, scopes=None):
|
||||
"""
|
||||
Checks if the access token is valid.
|
||||
|
||||
:param scopes: An iterable containing the scopes to check or None
|
||||
"""
|
||||
return not self.is_expired() and self.allow_scopes(scopes)
|
||||
|
||||
def is_expired(self):
|
||||
"""
|
||||
Check token expiration with timezone awareness
|
||||
"""
|
||||
if not self.expires:
|
||||
return True
|
||||
|
||||
return timezone.now() >= self.expires
|
||||
|
||||
def allow_scopes(self, scopes):
|
||||
"""
|
||||
Check if the token allows the provided scopes
|
||||
|
||||
:param scopes: An iterable containing the scopes to check
|
||||
"""
|
||||
if not scopes:
|
||||
return True
|
||||
|
||||
provided_scopes = set(self.scope.split())
|
||||
resource_scopes = set(scopes)
|
||||
|
||||
return resource_scopes.issubset(provided_scopes)
|
||||
|
||||
def revoke(self):
|
||||
"""
|
||||
Convenience method to uniform tokens' interface, for now
|
||||
simply remove this token from the database in order to revoke it.
|
||||
"""
|
||||
self.delete()
|
||||
|
||||
@property
|
||||
def scopes(self):
|
||||
"""
|
||||
Returns a dictionary of allowed scope names (as keys) with their descriptions (as values)
|
||||
"""
|
||||
all_scopes = get_scopes_backend().get_all_scopes()
|
||||
token_scopes = self.scope.split()
|
||||
return {name: desc for name, desc in all_scopes.items() if name in token_scopes}
|
||||
|
||||
def __str__(self):
|
||||
return "JTI: {self.jti} User: {self.user_id}".format(self=self)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class IDToken(AbstractIDToken):
|
||||
class Meta(AbstractIDToken.Meta):
|
||||
swappable = "OAUTH2_PROVIDER_ID_TOKEN_MODEL"
|
||||
|
||||
|
||||
def get_application_model():
|
||||
"""Return the Application model that is active in this project."""
|
||||
return apps.get_model(oauth2_settings.APPLICATION_MODEL)
|
||||
|
||||
|
||||
def get_grant_model():
|
||||
"""Return the Grant model that is active in this project."""
|
||||
return apps.get_model(oauth2_settings.GRANT_MODEL)
|
||||
|
||||
|
||||
def get_access_token_model():
|
||||
"""Return the AccessToken model that is active in this project."""
|
||||
return apps.get_model(oauth2_settings.ACCESS_TOKEN_MODEL)
|
||||
|
||||
|
||||
def get_id_token_model():
|
||||
"""Return the IDToken model that is active in this project."""
|
||||
return apps.get_model(oauth2_settings.ID_TOKEN_MODEL)
|
||||
|
||||
|
||||
def get_refresh_token_model():
|
||||
"""Return the RefreshToken model that is active in this project."""
|
||||
return apps.get_model(oauth2_settings.REFRESH_TOKEN_MODEL)
|
||||
|
||||
|
||||
def get_application_admin_class():
|
||||
"""Return the Application admin class that is active in this project."""
|
||||
application_admin_class = oauth2_settings.APPLICATION_ADMIN_CLASS
|
||||
return application_admin_class
|
||||
|
||||
|
||||
def get_access_token_admin_class():
|
||||
"""Return the AccessToken admin class that is active in this project."""
|
||||
access_token_admin_class = oauth2_settings.ACCESS_TOKEN_ADMIN_CLASS
|
||||
return access_token_admin_class
|
||||
|
||||
|
||||
def get_grant_admin_class():
|
||||
"""Return the Grant admin class that is active in this project."""
|
||||
grant_admin_class = oauth2_settings.GRANT_ADMIN_CLASS
|
||||
return grant_admin_class
|
||||
|
||||
|
||||
def get_id_token_admin_class():
|
||||
"""Return the IDToken admin class that is active in this project."""
|
||||
id_token_admin_class = oauth2_settings.ID_TOKEN_ADMIN_CLASS
|
||||
return id_token_admin_class
|
||||
|
||||
|
||||
def get_refresh_token_admin_class():
|
||||
"""Return the RefreshToken admin class that is active in this project."""
|
||||
refresh_token_admin_class = oauth2_settings.REFRESH_TOKEN_ADMIN_CLASS
|
||||
return refresh_token_admin_class
|
||||
|
||||
|
||||
def clear_expired():
|
||||
def batch_delete(queryset, query):
|
||||
CLEAR_EXPIRED_TOKENS_BATCH_SIZE = oauth2_settings.CLEAR_EXPIRED_TOKENS_BATCH_SIZE
|
||||
CLEAR_EXPIRED_TOKENS_BATCH_INTERVAL = oauth2_settings.CLEAR_EXPIRED_TOKENS_BATCH_INTERVAL
|
||||
current_no = start_no = queryset.count()
|
||||
|
||||
while current_no:
|
||||
flat_queryset = queryset.values_list("id", flat=True)[:CLEAR_EXPIRED_TOKENS_BATCH_SIZE]
|
||||
batch_length = flat_queryset.count()
|
||||
queryset.model.objects.filter(id__in=list(flat_queryset)).delete()
|
||||
logger.debug(f"{batch_length} tokens deleted, {current_no-batch_length} left")
|
||||
queryset = queryset.model.objects.filter(query)
|
||||
time.sleep(CLEAR_EXPIRED_TOKENS_BATCH_INTERVAL)
|
||||
current_no = queryset.count()
|
||||
|
||||
stop_no = queryset.model.objects.filter(query).count()
|
||||
deleted = start_no - stop_no
|
||||
return deleted
|
||||
|
||||
now = timezone.now()
|
||||
refresh_expire_at = None
|
||||
access_token_model = get_access_token_model()
|
||||
refresh_token_model = get_refresh_token_model()
|
||||
id_token_model = get_id_token_model()
|
||||
grant_model = get_grant_model()
|
||||
REFRESH_TOKEN_EXPIRE_SECONDS = oauth2_settings.REFRESH_TOKEN_EXPIRE_SECONDS
|
||||
|
||||
if REFRESH_TOKEN_EXPIRE_SECONDS:
|
||||
if not isinstance(REFRESH_TOKEN_EXPIRE_SECONDS, timedelta):
|
||||
try:
|
||||
REFRESH_TOKEN_EXPIRE_SECONDS = timedelta(seconds=REFRESH_TOKEN_EXPIRE_SECONDS)
|
||||
except TypeError:
|
||||
e = "REFRESH_TOKEN_EXPIRE_SECONDS must be either a timedelta or seconds"
|
||||
raise ImproperlyConfigured(e)
|
||||
refresh_expire_at = now - REFRESH_TOKEN_EXPIRE_SECONDS
|
||||
|
||||
if refresh_expire_at:
|
||||
revoked_query = models.Q(revoked__lt=refresh_expire_at)
|
||||
revoked = refresh_token_model.objects.filter(revoked_query)
|
||||
|
||||
revoked_deleted_no = batch_delete(revoked, revoked_query)
|
||||
logger.info("%s Revoked refresh tokens deleted", revoked_deleted_no)
|
||||
|
||||
expired_query = models.Q(access_token__expires__lt=refresh_expire_at)
|
||||
expired = refresh_token_model.objects.filter(expired_query)
|
||||
|
||||
expired_deleted_no = batch_delete(expired, expired_query)
|
||||
logger.info("%s Expired refresh tokens deleted", expired_deleted_no)
|
||||
else:
|
||||
logger.info("refresh_expire_at is %s. No refresh tokens deleted.", refresh_expire_at)
|
||||
|
||||
access_token_query = models.Q(refresh_token__isnull=True, expires__lt=now)
|
||||
access_tokens = access_token_model.objects.filter(access_token_query)
|
||||
|
||||
access_tokens_delete_no = batch_delete(access_tokens, access_token_query)
|
||||
logger.info("%s Expired access tokens deleted", access_tokens_delete_no)
|
||||
|
||||
id_token_query = models.Q(access_token__isnull=True, expires__lt=now)
|
||||
id_tokens = id_token_model.objects.filter(id_token_query)
|
||||
|
||||
id_tokens_delete_no = batch_delete(id_tokens, id_token_query)
|
||||
logger.info("%s Expired ID tokens deleted", id_tokens_delete_no)
|
||||
|
||||
grants_query = models.Q(expires__lt=now)
|
||||
grants = grant_model.objects.filter(grants_query)
|
||||
|
||||
grants_deleted_no = batch_delete(grants, grants_query)
|
||||
logger.info("%s Expired grant tokens deleted", grants_deleted_no)
|
||||
|
||||
|
||||
def redirect_to_uri_allowed(uri, allowed_uris):
|
||||
"""
|
||||
Checks if a given uri can be redirected to based on the provided allowed_uris configuration.
|
||||
|
||||
On top of exact matches, this function also handles loopback IPs based on RFC 8252.
|
||||
|
||||
:param uri: URI to check
|
||||
:param allowed_uris: A list of URIs that are allowed
|
||||
"""
|
||||
|
||||
parsed_uri = urlparse(uri)
|
||||
uqs_set = set(parse_qsl(parsed_uri.query))
|
||||
for allowed_uri in allowed_uris:
|
||||
parsed_allowed_uri = urlparse(allowed_uri)
|
||||
|
||||
# From RFC 8252 (Section 7.3)
|
||||
#
|
||||
# Loopback redirect URIs use the "http" scheme
|
||||
# [...]
|
||||
# The authorization server MUST allow any port to be specified at the
|
||||
# time of the request for loopback IP redirect URIs, to accommodate
|
||||
# clients that obtain an available ephemeral port from the operating
|
||||
# system at the time of the request.
|
||||
|
||||
allowed_uri_is_loopback = (
|
||||
parsed_allowed_uri.scheme == "http"
|
||||
and parsed_allowed_uri.hostname in ["127.0.0.1", "::1"]
|
||||
and parsed_allowed_uri.port is None
|
||||
)
|
||||
if (
|
||||
allowed_uri_is_loopback
|
||||
and parsed_allowed_uri.scheme == parsed_uri.scheme
|
||||
and parsed_allowed_uri.hostname == parsed_uri.hostname
|
||||
and parsed_allowed_uri.path == parsed_uri.path
|
||||
) or (
|
||||
parsed_allowed_uri.scheme == parsed_uri.scheme
|
||||
and parsed_allowed_uri.netloc == parsed_uri.netloc
|
||||
and parsed_allowed_uri.path == parsed_uri.path
|
||||
):
|
||||
aqs_set = set(parse_qsl(parsed_allowed_uri.query))
|
||||
if aqs_set.issubset(uqs_set):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def is_origin_allowed(origin, allowed_origins):
|
||||
"""
|
||||
Checks if a given origin uri is allowed based on the provided allowed_origins configuration.
|
||||
|
||||
:param origin: Origin URI to check
|
||||
:param allowed_origins: A list of Origin URIs that are allowed
|
||||
"""
|
||||
|
||||
parsed_origin = urlparse(origin)
|
||||
|
||||
if parsed_origin.scheme not in oauth2_settings.ALLOWED_SCHEMES:
|
||||
return False
|
||||
|
||||
for allowed_origin in allowed_origins:
|
||||
parsed_allowed_origin = urlparse(allowed_origin)
|
||||
if (
|
||||
parsed_allowed_origin.scheme == parsed_origin.scheme
|
||||
and parsed_allowed_origin.netloc == parsed_origin.netloc
|
||||
):
|
||||
return True
|
||||
return False
|
||||
@@ -0,0 +1,250 @@
|
||||
import json
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
from oauthlib import oauth2
|
||||
from oauthlib.common import Request as OauthlibRequest
|
||||
from oauthlib.common import quote, urlencode, urlencoded
|
||||
from oauthlib.oauth2 import OAuth2Error
|
||||
|
||||
from .exceptions import FatalClientError, OAuthToolkitError
|
||||
from .settings import oauth2_settings
|
||||
|
||||
|
||||
class OAuthLibCore:
|
||||
"""
|
||||
Wrapper for oauth Server providing django-specific interfaces.
|
||||
|
||||
Meant for things like extracting request data and converting
|
||||
everything to formats more palatable for oauthlib's Server.
|
||||
"""
|
||||
|
||||
def __init__(self, server=None):
|
||||
"""
|
||||
:params server: An instance of oauthlib.oauth2.Server class
|
||||
"""
|
||||
validator_class = oauth2_settings.OAUTH2_VALIDATOR_CLASS
|
||||
validator = validator_class()
|
||||
server_kwargs = oauth2_settings.server_kwargs
|
||||
self.server = server or oauth2_settings.OAUTH2_SERVER_CLASS(validator, **server_kwargs)
|
||||
|
||||
def _get_escaped_full_path(self, request):
|
||||
"""
|
||||
Django considers "safe" some characters that aren't so for oauthlib.
|
||||
We have to search for them and properly escape.
|
||||
"""
|
||||
parsed = list(urlparse(request.get_full_path()))
|
||||
unsafe = set(c for c in parsed[4]).difference(urlencoded)
|
||||
for c in unsafe:
|
||||
parsed[4] = parsed[4].replace(c, quote(c, safe=b""))
|
||||
|
||||
return urlunparse(parsed)
|
||||
|
||||
def _get_extra_credentials(self, request):
|
||||
"""
|
||||
Produce extra credentials for token response. This dictionary will be
|
||||
merged with the response.
|
||||
See also: `oauthlib.oauth2.rfc6749.TokenEndpoint.create_token_response`
|
||||
|
||||
:param request: The current django.http.HttpRequest object
|
||||
:return: dictionary of extra credentials or None (default)
|
||||
"""
|
||||
return None
|
||||
|
||||
def _extract_params(self, request):
|
||||
"""
|
||||
Extract parameters from the Django request object.
|
||||
Such parameters will then be passed to OAuthLib to build its own
|
||||
Request object. The body should be encoded using OAuthLib urlencoded.
|
||||
"""
|
||||
uri = self._get_escaped_full_path(request)
|
||||
http_method = request.method
|
||||
headers = self.extract_headers(request)
|
||||
body = urlencode(self.extract_body(request))
|
||||
return uri, http_method, body, headers
|
||||
|
||||
def extract_headers(self, request):
|
||||
"""
|
||||
Extracts headers from the Django request object
|
||||
:param request: The current django.http.HttpRequest object
|
||||
:return: a dictionary with OAuthLib needed headers
|
||||
"""
|
||||
headers = request.META.copy()
|
||||
if "wsgi.input" in headers:
|
||||
del headers["wsgi.input"]
|
||||
if "wsgi.errors" in headers:
|
||||
del headers["wsgi.errors"]
|
||||
if "HTTP_AUTHORIZATION" in headers:
|
||||
headers["Authorization"] = headers["HTTP_AUTHORIZATION"]
|
||||
# Add Access-Control-Allow-Origin header to the token endpoint response for authentication code grant,
|
||||
# if the origin is allowed by RequestValidator.is_origin_allowed.
|
||||
# https://github.com/oauthlib/oauthlib/pull/791
|
||||
if "HTTP_ORIGIN" in headers:
|
||||
headers["Origin"] = headers["HTTP_ORIGIN"]
|
||||
if request.is_secure():
|
||||
headers["X_DJANGO_OAUTH_TOOLKIT_SECURE"] = "1"
|
||||
elif "X_DJANGO_OAUTH_TOOLKIT_SECURE" in headers:
|
||||
del headers["X_DJANGO_OAUTH_TOOLKIT_SECURE"]
|
||||
|
||||
return headers
|
||||
|
||||
def extract_body(self, request):
|
||||
"""
|
||||
Extracts the POST body from the Django request object
|
||||
:param request: The current django.http.HttpRequest object
|
||||
:return: provided POST parameters
|
||||
"""
|
||||
return request.POST.items()
|
||||
|
||||
def validate_authorization_request(self, request):
|
||||
"""
|
||||
A wrapper method that calls validate_authorization_request on `server_class` instance.
|
||||
|
||||
:param request: The current django.http.HttpRequest object
|
||||
"""
|
||||
try:
|
||||
uri, http_method, body, headers = self._extract_params(request)
|
||||
scopes, credentials = self.server.validate_authorization_request(
|
||||
uri, http_method=http_method, body=body, headers=headers
|
||||
)
|
||||
|
||||
return scopes, credentials
|
||||
except oauth2.FatalClientError as error:
|
||||
raise FatalClientError(error=error)
|
||||
except oauth2.OAuth2Error as error:
|
||||
raise OAuthToolkitError(error=error)
|
||||
|
||||
def create_authorization_response(self, request, scopes, credentials, allow):
|
||||
"""
|
||||
A wrapper method that calls create_authorization_response on `server_class`
|
||||
instance.
|
||||
|
||||
:param request: The current django.http.HttpRequest object
|
||||
:param scopes: A list of provided scopes
|
||||
:param credentials: Authorization credentials dictionary containing
|
||||
`client_id`, `state`, `redirect_uri`, `response_type`
|
||||
:param allow: True if the user authorize the client, otherwise False
|
||||
"""
|
||||
try:
|
||||
if not allow:
|
||||
raise oauth2.AccessDeniedError(state=credentials.get("state", None))
|
||||
|
||||
# add current user to credentials. this will be used by OAUTH2_VALIDATOR_CLASS
|
||||
credentials["user"] = request.user
|
||||
request_uri, http_method, _, request_headers = self._extract_params(request)
|
||||
|
||||
headers, body, status = self.server.create_authorization_response(
|
||||
uri=request_uri,
|
||||
http_method=http_method,
|
||||
headers=request_headers,
|
||||
scopes=scopes,
|
||||
credentials=credentials,
|
||||
)
|
||||
uri = headers.get("Location", None)
|
||||
|
||||
return uri, headers, body, status
|
||||
|
||||
except oauth2.FatalClientError as error:
|
||||
raise FatalClientError(error=error, redirect_uri=credentials["redirect_uri"])
|
||||
except oauth2.OAuth2Error as error:
|
||||
raise OAuthToolkitError(error=error, redirect_uri=credentials["redirect_uri"])
|
||||
|
||||
def create_token_response(self, request):
|
||||
"""
|
||||
A wrapper method that calls create_token_response on `server_class` instance.
|
||||
|
||||
:param request: The current django.http.HttpRequest object
|
||||
"""
|
||||
uri, http_method, body, headers = self._extract_params(request)
|
||||
extra_credentials = self._get_extra_credentials(request)
|
||||
|
||||
try:
|
||||
headers, body, status = self.server.create_token_response(
|
||||
uri, http_method, body, headers, extra_credentials
|
||||
)
|
||||
uri = headers.get("Location", None)
|
||||
return uri, headers, body, status
|
||||
except OAuth2Error as exc:
|
||||
return None, exc.headers, exc.json, exc.status_code
|
||||
|
||||
def create_revocation_response(self, request):
|
||||
"""
|
||||
A wrapper method that calls create_revocation_response on a
|
||||
`server_class` instance.
|
||||
|
||||
:param request: The current django.http.HttpRequest object
|
||||
"""
|
||||
uri, http_method, body, headers = self._extract_params(request)
|
||||
|
||||
headers, body, status = self.server.create_revocation_response(uri, http_method, body, headers)
|
||||
uri = headers.get("Location", None)
|
||||
|
||||
return uri, headers, body, status
|
||||
|
||||
def create_userinfo_response(self, request):
|
||||
"""
|
||||
A wrapper method that calls create_userinfo_response on a
|
||||
`server_class` instance.
|
||||
|
||||
:param request: The current django.http.HttpRequest object
|
||||
"""
|
||||
uri, http_method, body, headers = self._extract_params(request)
|
||||
try:
|
||||
headers, body, status = self.server.create_userinfo_response(uri, http_method, body, headers)
|
||||
uri = headers.get("Location", None)
|
||||
return uri, headers, body, status
|
||||
except OAuth2Error as exc:
|
||||
return None, exc.headers, exc.json, exc.status_code
|
||||
|
||||
def verify_request(self, request, scopes):
|
||||
"""
|
||||
A wrapper method that calls verify_request on `server_class` instance.
|
||||
|
||||
:param request: The current django.http.HttpRequest object
|
||||
:param scopes: A list of scopes required to verify so that request is verified
|
||||
"""
|
||||
uri, http_method, body, headers = self._extract_params(request)
|
||||
|
||||
valid, r = self.server.verify_request(uri, http_method, body, headers, scopes=scopes)
|
||||
return valid, r
|
||||
|
||||
def authenticate_client(self, request):
|
||||
"""Wrapper to call `authenticate_client` on `server_class` instance.
|
||||
|
||||
:param request: The current django.http.HttpRequest object
|
||||
"""
|
||||
uri, http_method, body, headers = self._extract_params(request)
|
||||
oauth_request = OauthlibRequest(uri, http_method, body, headers)
|
||||
return self.server.request_validator.authenticate_client(oauth_request)
|
||||
|
||||
|
||||
class JSONOAuthLibCore(OAuthLibCore):
|
||||
"""
|
||||
Extends the default OAuthLibCore to parse correctly application/json requests
|
||||
"""
|
||||
|
||||
def extract_body(self, request):
|
||||
"""
|
||||
Extracts the JSON body from the Django request object
|
||||
:param request: The current django.http.HttpRequest object
|
||||
:return: provided POST parameters "urlencodable"
|
||||
"""
|
||||
try:
|
||||
body = json.loads(request.body.decode("utf-8")).items()
|
||||
except AttributeError:
|
||||
body = ""
|
||||
except ValueError:
|
||||
body = ""
|
||||
|
||||
return body
|
||||
|
||||
|
||||
def get_oauthlib_core():
|
||||
"""
|
||||
Utility function that returns an instance of
|
||||
`oauth2_provider.backends.OAuthLibCore`
|
||||
"""
|
||||
validator_class = oauth2_settings.OAUTH2_VALIDATOR_CLASS
|
||||
validator = validator_class()
|
||||
server_kwargs = oauth2_settings.server_kwargs
|
||||
server = oauth2_settings.OAUTH2_SERVER_CLASS(validator, **server_kwargs)
|
||||
return oauth2_settings.OAUTH2_BACKEND_CLASS(server)
|
||||
File diff suppressed because it is too large
Load Diff
50
venv/lib/python3.11/site-packages/oauth2_provider/scopes.py
Normal file
50
venv/lib/python3.11/site-packages/oauth2_provider/scopes.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from .settings import oauth2_settings
|
||||
|
||||
|
||||
class BaseScopes:
|
||||
def get_all_scopes(self):
|
||||
"""
|
||||
Return a dict-like object with all the scopes available in the
|
||||
system. The key should be the scope name and the value should be
|
||||
the description.
|
||||
|
||||
ex: {"read": "A read scope", "write": "A write scope"}
|
||||
"""
|
||||
raise NotImplementedError("")
|
||||
|
||||
def get_available_scopes(self, application=None, request=None, *args, **kwargs):
|
||||
"""
|
||||
Return a list of scopes available for the current application/request.
|
||||
|
||||
TODO: add info on where and why this method is called.
|
||||
|
||||
ex: ["read", "write"]
|
||||
"""
|
||||
raise NotImplementedError("")
|
||||
|
||||
def get_default_scopes(self, application=None, request=None, *args, **kwargs):
|
||||
"""
|
||||
Return a list of the default scopes for the current application/request.
|
||||
This MUST be a subset of the scopes returned by `get_available_scopes`.
|
||||
|
||||
TODO: add info on where and why this method is called.
|
||||
|
||||
ex: ["read"]
|
||||
"""
|
||||
raise NotImplementedError("")
|
||||
|
||||
|
||||
class SettingsScopes(BaseScopes):
|
||||
def get_all_scopes(self):
|
||||
return oauth2_settings.SCOPES
|
||||
|
||||
def get_available_scopes(self, application=None, request=None, *args, **kwargs):
|
||||
return oauth2_settings._SCOPES
|
||||
|
||||
def get_default_scopes(self, application=None, request=None, *args, **kwargs):
|
||||
return oauth2_settings._DEFAULT_SCOPES
|
||||
|
||||
|
||||
def get_scopes_backend():
|
||||
scopes_class = oauth2_settings.SCOPES_BACKEND_CLASS
|
||||
return scopes_class()
|
||||
313
venv/lib/python3.11/site-packages/oauth2_provider/settings.py
Normal file
313
venv/lib/python3.11/site-packages/oauth2_provider/settings.py
Normal file
@@ -0,0 +1,313 @@
|
||||
"""
|
||||
This module is largely inspired by django-rest-framework settings.
|
||||
|
||||
Settings for the OAuth2 Provider are all namespaced in the OAUTH2_PROVIDER setting.
|
||||
For example your project's `settings.py` file might look like this:
|
||||
|
||||
OAUTH2_PROVIDER = {
|
||||
"CLIENT_ID_GENERATOR_CLASS":
|
||||
"oauth2_provider.generators.ClientIdGenerator",
|
||||
"CLIENT_SECRET_GENERATOR_CLASS":
|
||||
"oauth2_provider.generators.ClientSecretGenerator",
|
||||
}
|
||||
|
||||
This module provides the `oauth2_settings` object, that is used to access
|
||||
OAuth2 Provider settings, checking for user settings first, then falling
|
||||
back to the defaults.
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.signals import setting_changed
|
||||
from django.http import HttpRequest
|
||||
from django.urls import reverse
|
||||
from django.utils.module_loading import import_string
|
||||
from oauthlib.common import Request
|
||||
|
||||
|
||||
USER_SETTINGS = getattr(settings, "OAUTH2_PROVIDER", None)
|
||||
|
||||
APPLICATION_MODEL = getattr(settings, "OAUTH2_PROVIDER_APPLICATION_MODEL", "oauth2_provider.Application")
|
||||
ACCESS_TOKEN_MODEL = getattr(settings, "OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL", "oauth2_provider.AccessToken")
|
||||
ID_TOKEN_MODEL = getattr(settings, "OAUTH2_PROVIDER_ID_TOKEN_MODEL", "oauth2_provider.IDToken")
|
||||
GRANT_MODEL = getattr(settings, "OAUTH2_PROVIDER_GRANT_MODEL", "oauth2_provider.Grant")
|
||||
REFRESH_TOKEN_MODEL = getattr(settings, "OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL", "oauth2_provider.RefreshToken")
|
||||
|
||||
DEFAULTS = {
|
||||
"CLIENT_ID_GENERATOR_CLASS": "oauth2_provider.generators.ClientIdGenerator",
|
||||
"CLIENT_SECRET_GENERATOR_CLASS": "oauth2_provider.generators.ClientSecretGenerator",
|
||||
"CLIENT_SECRET_GENERATOR_LENGTH": 128,
|
||||
"ACCESS_TOKEN_GENERATOR": None,
|
||||
"REFRESH_TOKEN_GENERATOR": None,
|
||||
"EXTRA_SERVER_KWARGS": {},
|
||||
"OAUTH2_SERVER_CLASS": "oauthlib.oauth2.Server",
|
||||
"OIDC_SERVER_CLASS": "oauthlib.openid.Server",
|
||||
"OAUTH2_VALIDATOR_CLASS": "oauth2_provider.oauth2_validators.OAuth2Validator",
|
||||
"OAUTH2_BACKEND_CLASS": "oauth2_provider.oauth2_backends.OAuthLibCore",
|
||||
"SCOPES": {"read": "Reading scope", "write": "Writing scope"},
|
||||
"DEFAULT_SCOPES": ["__all__"],
|
||||
"SCOPES_BACKEND_CLASS": "oauth2_provider.scopes.SettingsScopes",
|
||||
"READ_SCOPE": "read",
|
||||
"WRITE_SCOPE": "write",
|
||||
"AUTHORIZATION_CODE_EXPIRE_SECONDS": 60,
|
||||
"ACCESS_TOKEN_EXPIRE_SECONDS": 36000,
|
||||
"ID_TOKEN_EXPIRE_SECONDS": 36000,
|
||||
"REFRESH_TOKEN_EXPIRE_SECONDS": None,
|
||||
"REFRESH_TOKEN_GRACE_PERIOD_SECONDS": 0,
|
||||
"REFRESH_TOKEN_REUSE_PROTECTION": False,
|
||||
"ROTATE_REFRESH_TOKEN": True,
|
||||
"ERROR_RESPONSE_WITH_SCOPES": False,
|
||||
"APPLICATION_MODEL": APPLICATION_MODEL,
|
||||
"ACCESS_TOKEN_MODEL": ACCESS_TOKEN_MODEL,
|
||||
"ID_TOKEN_MODEL": ID_TOKEN_MODEL,
|
||||
"GRANT_MODEL": GRANT_MODEL,
|
||||
"REFRESH_TOKEN_MODEL": REFRESH_TOKEN_MODEL,
|
||||
"APPLICATION_ADMIN_CLASS": "oauth2_provider.admin.ApplicationAdmin",
|
||||
"ACCESS_TOKEN_ADMIN_CLASS": "oauth2_provider.admin.AccessTokenAdmin",
|
||||
"GRANT_ADMIN_CLASS": "oauth2_provider.admin.GrantAdmin",
|
||||
"ID_TOKEN_ADMIN_CLASS": "oauth2_provider.admin.IDTokenAdmin",
|
||||
"REFRESH_TOKEN_ADMIN_CLASS": "oauth2_provider.admin.RefreshTokenAdmin",
|
||||
"REQUEST_APPROVAL_PROMPT": "force",
|
||||
"ALLOWED_REDIRECT_URI_SCHEMES": ["http", "https"],
|
||||
"ALLOWED_SCHEMES": ["https"],
|
||||
"OIDC_ENABLED": False,
|
||||
"OIDC_ISS_ENDPOINT": "",
|
||||
"OIDC_USERINFO_ENDPOINT": "",
|
||||
"OIDC_RSA_PRIVATE_KEY": "",
|
||||
"OIDC_RSA_PRIVATE_KEYS_INACTIVE": [],
|
||||
"OIDC_JWKS_MAX_AGE_SECONDS": 3600,
|
||||
"OIDC_RESPONSE_TYPES_SUPPORTED": [
|
||||
"code",
|
||||
"token",
|
||||
"id_token",
|
||||
"id_token token",
|
||||
"code token",
|
||||
"code id_token",
|
||||
"code id_token token",
|
||||
],
|
||||
"OIDC_SUBJECT_TYPES_SUPPORTED": ["public"],
|
||||
"OIDC_TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED": [
|
||||
"client_secret_post",
|
||||
"client_secret_basic",
|
||||
],
|
||||
"OIDC_RP_INITIATED_LOGOUT_ENABLED": False,
|
||||
"OIDC_RP_INITIATED_LOGOUT_ALWAYS_PROMPT": True,
|
||||
"OIDC_RP_INITIATED_LOGOUT_STRICT_REDIRECT_URIS": False,
|
||||
"OIDC_RP_INITIATED_LOGOUT_ACCEPT_EXPIRED_TOKENS": True,
|
||||
"OIDC_RP_INITIATED_LOGOUT_DELETE_TOKENS": True,
|
||||
# Special settings that will be evaluated at runtime
|
||||
"_SCOPES": [],
|
||||
"_DEFAULT_SCOPES": [],
|
||||
# Resource Server with Token Introspection
|
||||
"RESOURCE_SERVER_INTROSPECTION_URL": None,
|
||||
"RESOURCE_SERVER_AUTH_TOKEN": None,
|
||||
"RESOURCE_SERVER_INTROSPECTION_CREDENTIALS": None,
|
||||
"RESOURCE_SERVER_TOKEN_CACHING_SECONDS": 36000,
|
||||
# Authentication Server Exp Timezone: the time zone use dby Auth Server for generate EXP
|
||||
"AUTHENTICATION_SERVER_EXP_TIME_ZONE": "UTC",
|
||||
# Whether or not PKCE is required
|
||||
"PKCE_REQUIRED": True,
|
||||
# Whether to re-create OAuthlibCore on every request.
|
||||
# Should only be required in testing.
|
||||
"ALWAYS_RELOAD_OAUTHLIB_CORE": False,
|
||||
"CLEAR_EXPIRED_TOKENS_BATCH_SIZE": 10000,
|
||||
"CLEAR_EXPIRED_TOKENS_BATCH_INTERVAL": 0,
|
||||
}
|
||||
|
||||
# List of settings that cannot be empty
|
||||
MANDATORY = (
|
||||
"CLIENT_ID_GENERATOR_CLASS",
|
||||
"CLIENT_SECRET_GENERATOR_CLASS",
|
||||
"OAUTH2_SERVER_CLASS",
|
||||
"OAUTH2_VALIDATOR_CLASS",
|
||||
"OAUTH2_BACKEND_CLASS",
|
||||
"SCOPES",
|
||||
"ALLOWED_REDIRECT_URI_SCHEMES",
|
||||
"OIDC_RESPONSE_TYPES_SUPPORTED",
|
||||
"OIDC_SUBJECT_TYPES_SUPPORTED",
|
||||
"OIDC_TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED",
|
||||
)
|
||||
|
||||
# List of settings that may be in string import notation.
|
||||
IMPORT_STRINGS = (
|
||||
"CLIENT_ID_GENERATOR_CLASS",
|
||||
"CLIENT_SECRET_GENERATOR_CLASS",
|
||||
"ACCESS_TOKEN_GENERATOR",
|
||||
"REFRESH_TOKEN_GENERATOR",
|
||||
"OAUTH2_SERVER_CLASS",
|
||||
"OAUTH2_VALIDATOR_CLASS",
|
||||
"OAUTH2_BACKEND_CLASS",
|
||||
"SCOPES_BACKEND_CLASS",
|
||||
"APPLICATION_ADMIN_CLASS",
|
||||
"ACCESS_TOKEN_ADMIN_CLASS",
|
||||
"GRANT_ADMIN_CLASS",
|
||||
"ID_TOKEN_ADMIN_CLASS",
|
||||
"REFRESH_TOKEN_ADMIN_CLASS",
|
||||
)
|
||||
|
||||
|
||||
def perform_import(val, setting_name):
|
||||
"""
|
||||
If the given setting is a string import notation,
|
||||
then perform the necessary import or imports.
|
||||
"""
|
||||
if val is None:
|
||||
return None
|
||||
elif isinstance(val, str):
|
||||
return import_from_string(val, setting_name)
|
||||
elif isinstance(val, (list, tuple)):
|
||||
return [import_from_string(item, setting_name) for item in val]
|
||||
return val
|
||||
|
||||
|
||||
def import_from_string(val, setting_name):
|
||||
"""
|
||||
Attempt to import a class from a string representation.
|
||||
"""
|
||||
try:
|
||||
return import_string(val)
|
||||
except ImportError as e:
|
||||
msg = "Could not import %r for setting %r. %s: %s." % (val, setting_name, e.__class__.__name__, e)
|
||||
raise ImportError(msg)
|
||||
|
||||
|
||||
class _PhonyHttpRequest(HttpRequest):
|
||||
_scheme = "http"
|
||||
|
||||
def _get_scheme(self):
|
||||
return self._scheme
|
||||
|
||||
|
||||
class OAuth2ProviderSettings:
|
||||
"""
|
||||
A settings object, that allows OAuth2 Provider settings to be accessed as properties.
|
||||
|
||||
Any setting with string import paths will be automatically resolved
|
||||
and return the class, rather than the string literal.
|
||||
"""
|
||||
|
||||
def __init__(self, user_settings=None, defaults=None, import_strings=None, mandatory=None):
|
||||
self._user_settings = user_settings or {}
|
||||
self.defaults = defaults or DEFAULTS
|
||||
self.import_strings = import_strings or IMPORT_STRINGS
|
||||
self.mandatory = mandatory or ()
|
||||
self._cached_attrs = set()
|
||||
|
||||
@property
|
||||
def user_settings(self):
|
||||
if not hasattr(self, "_user_settings"):
|
||||
self._user_settings = getattr(settings, "OAUTH2_PROVIDER", {})
|
||||
return self._user_settings
|
||||
|
||||
def __getattr__(self, attr):
|
||||
if attr not in self.defaults:
|
||||
raise AttributeError("Invalid OAuth2Provider setting: %s" % attr)
|
||||
try:
|
||||
# Check if present in user settings
|
||||
val = self.user_settings[attr]
|
||||
except KeyError:
|
||||
# Fall back to defaults
|
||||
# Special case OAUTH2_SERVER_CLASS - if not specified, and OIDC is
|
||||
# enabled, use the OIDC_SERVER_CLASS setting instead
|
||||
if attr == "OAUTH2_SERVER_CLASS" and self.OIDC_ENABLED:
|
||||
val = self.defaults["OIDC_SERVER_CLASS"]
|
||||
else:
|
||||
val = self.defaults[attr]
|
||||
|
||||
# Coerce import strings into classes
|
||||
if val and attr in self.import_strings:
|
||||
val = perform_import(val, attr)
|
||||
|
||||
# Overriding special settings
|
||||
if attr == "_SCOPES":
|
||||
val = list(self.SCOPES.keys())
|
||||
if attr == "_DEFAULT_SCOPES":
|
||||
if "__all__" in self.DEFAULT_SCOPES:
|
||||
# If DEFAULT_SCOPES is set to ["__all__"] the whole set of scopes is returned
|
||||
val = list(self._SCOPES)
|
||||
else:
|
||||
# Otherwise we return a subset (that can be void) of SCOPES
|
||||
val = []
|
||||
for scope in self.DEFAULT_SCOPES:
|
||||
if scope in self._SCOPES:
|
||||
val.append(scope)
|
||||
else:
|
||||
raise ImproperlyConfigured("Defined DEFAULT_SCOPES not present in SCOPES")
|
||||
|
||||
self.validate_setting(attr, val)
|
||||
|
||||
# Cache the result
|
||||
self._cached_attrs.add(attr)
|
||||
setattr(self, attr, val)
|
||||
return val
|
||||
|
||||
def validate_setting(self, attr, val):
|
||||
if not val and attr in self.mandatory:
|
||||
raise AttributeError("OAuth2Provider setting: %s is mandatory" % attr)
|
||||
|
||||
@property
|
||||
def server_kwargs(self):
|
||||
"""
|
||||
This is used to communicate settings to oauth server.
|
||||
|
||||
Takes relevant settings and format them accordingly.
|
||||
There's also EXTRA_SERVER_KWARGS that can override every value
|
||||
and is more flexible regarding keys and acceptable values
|
||||
but doesn't have import string magic or any additional
|
||||
processing, callables have to be assigned directly.
|
||||
For the likes of signed_token_generator it means something like
|
||||
|
||||
{"token_generator": signed_token_generator(privkey, **kwargs)}
|
||||
"""
|
||||
kwargs = {
|
||||
key: getattr(self, value)
|
||||
for key, value in [
|
||||
("token_expires_in", "ACCESS_TOKEN_EXPIRE_SECONDS"),
|
||||
("refresh_token_expires_in", "REFRESH_TOKEN_EXPIRE_SECONDS"),
|
||||
("token_generator", "ACCESS_TOKEN_GENERATOR"),
|
||||
("refresh_token_generator", "REFRESH_TOKEN_GENERATOR"),
|
||||
]
|
||||
}
|
||||
kwargs.update(self.EXTRA_SERVER_KWARGS)
|
||||
return kwargs
|
||||
|
||||
def reload(self):
|
||||
for attr in self._cached_attrs:
|
||||
delattr(self, attr)
|
||||
self._cached_attrs.clear()
|
||||
if hasattr(self, "_user_settings"):
|
||||
delattr(self, "_user_settings")
|
||||
|
||||
def oidc_issuer(self, request):
|
||||
"""
|
||||
Helper function to get the OIDC issuer URL, either from the settings
|
||||
or constructing it from the passed request.
|
||||
|
||||
If only an oauthlib request is available, a dummy django request is
|
||||
built from that and used to generate the URL.
|
||||
"""
|
||||
if self.OIDC_ISS_ENDPOINT:
|
||||
return self.OIDC_ISS_ENDPOINT
|
||||
if isinstance(request, HttpRequest):
|
||||
django_request = request
|
||||
elif isinstance(request, Request):
|
||||
django_request = _PhonyHttpRequest()
|
||||
django_request.META = request.headers
|
||||
if request.headers.get("X_DJANGO_OAUTH_TOOLKIT_SECURE", False):
|
||||
django_request._scheme = "https"
|
||||
else:
|
||||
raise TypeError("request must be a django or oauthlib request: got %r" % request)
|
||||
abs_url = django_request.build_absolute_uri(reverse("oauth2_provider:oidc-connect-discovery-info"))
|
||||
return abs_url[: -len("/.well-known/openid-configuration")]
|
||||
|
||||
|
||||
oauth2_settings = OAuth2ProviderSettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS, MANDATORY)
|
||||
|
||||
|
||||
def reload_oauth2_settings(*args, **kwargs):
|
||||
setting = kwargs["setting"]
|
||||
if setting == "OAUTH2_PROVIDER":
|
||||
oauth2_settings.reload()
|
||||
|
||||
|
||||
setting_changed.connect(reload_oauth2_settings)
|
||||
@@ -0,0 +1,4 @@
|
||||
from django.dispatch import Signal
|
||||
|
||||
|
||||
app_authorized = Signal() # providing_args=["request", "token"]
|
||||
@@ -0,0 +1,18 @@
|
||||
{% extends "oauth2_provider/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% block content %}
|
||||
<div class="block-center">
|
||||
<h3 class="block-center-heading">{% trans "Are you sure to delete the application" %} {{ application.name }}?</h3>
|
||||
<form method="post" action="{% url 'oauth2_provider:delete' application.pk %}">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<a class="btn btn-large" href="{% url "oauth2_provider:list" %}">{% trans "Cancel" %}</a>
|
||||
<input type="submit" class="btn btn-large btn-danger" name="allow" value="{% trans 'Delete' %}"/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -0,0 +1,56 @@
|
||||
{% extends "oauth2_provider/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% block content %}
|
||||
<div class="block-center">
|
||||
<h3 class="block-center-heading">{{ application.name }}</h3>
|
||||
|
||||
<ul class="unstyled">
|
||||
<li>
|
||||
<p><b>{% trans "Client id" %}</b></p>
|
||||
<input class="input-block-level" type="text" value="{{ application.client_id }}" readonly>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<p><b>{% trans "Client secret" %}</b></p>
|
||||
<input class="input-block-level" type="text" value="{{ application.client_secret }}" readonly>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<p><b>{% trans "Hash client secret" %}</b></p>
|
||||
<p>{{ application.hash_client_secret|yesno:_("yes,no") }}</p>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<p><b>{% trans "Client type" %}</b></p>
|
||||
<p>{{ application.client_type }}</p>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<p><b>{% trans "Authorization Grant Type" %}</b></p>
|
||||
<p>{{ application.authorization_grant_type }}</p>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<p><b>{% trans "Redirect Uris" %}</b></p>
|
||||
<textarea class="input-block-level" readonly>{{ application.redirect_uris }}</textarea>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<p><b>{% trans "Post Logout Redirect Uris" %}</b></p>
|
||||
<textarea class="input-block-level" readonly>{{ application.post_logout_redirect_uris }}</textarea>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<p><b>{% trans "Allowed Origins" %}</b></p>
|
||||
<textarea class="input-block-level" readonly>{{ application.allowed_origins }}</textarea>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="btn-toolbar">
|
||||
<a class="btn" href="{% url "oauth2_provider:list" %}">{% trans "Go Back" %}</a>
|
||||
<a class="btn btn-primary" href="{% url "oauth2_provider:update" application.pk %}">{% trans "Edit" %}</a>
|
||||
<a class="btn btn-danger" href="{% url "oauth2_provider:delete" application.pk %}">{% trans "Delete" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -0,0 +1,42 @@
|
||||
{% extends "oauth2_provider/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% block content %}
|
||||
<div class="block-center">
|
||||
<form class="form-horizontal" method="post" action="{% block app-form-action-url %}{% url 'oauth2_provider:update' application.pk %}{% endblock app-form-action-url %}">
|
||||
<h3 class="block-center-heading">
|
||||
{% block app-form-title %}
|
||||
{% trans "Edit application" %} {{ application.name }}
|
||||
{% endblock app-form-title %}
|
||||
</h3>
|
||||
{% csrf_token %}
|
||||
|
||||
{% for field in form %}
|
||||
<div class="control-group {% if field.errors %}error{% endif %}">
|
||||
<label class="control-label" for="{{ field.id_for_label }}">{{ field.label }}</label>
|
||||
<div class="controls">
|
||||
{{ field }}
|
||||
{% for error in field.errors %}
|
||||
<span class="help-inline">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="control-group {% if form.non_field_errors %}error{% endif %}">
|
||||
{% for error in form.non_field_errors %}
|
||||
<span class="help-inline">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<a class="btn" href="{% block app-form-back-url %}{% url "oauth2_provider:detail" application.pk %}{% endblock app-form-back-url %}">
|
||||
{% trans "Go Back" %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary">{% trans "Save" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,20 @@
|
||||
{% extends "oauth2_provider/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% block content %}
|
||||
<div class="block-center">
|
||||
<h3 class="block-center-heading">{% trans "Your applications" %}</h3>
|
||||
{% if applications %}
|
||||
<ul>
|
||||
{% for application in applications %}
|
||||
<li><a href="{% url "oauth2_provider:detail" application.pk %}">{{ application.name }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<a class="btn btn-success" href="{% url "oauth2_provider:register" %}">{% trans "New Application" %}</a>
|
||||
{% else %}
|
||||
|
||||
<p>{% trans "No applications defined" %}. <a href="{% url 'oauth2_provider:register' %}">{% trans "Click here" %}</a> {% trans "if you want to register a new one" %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -0,0 +1,9 @@
|
||||
{% extends "oauth2_provider/application_form.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block app-form-title %}{% trans "Register a new application" %}{% endblock app-form-title %}
|
||||
|
||||
{% block app-form-action-url %}{% url 'oauth2_provider:register' %}{% endblock app-form-action-url %}
|
||||
|
||||
{% block app-form-back-url %}{% url "oauth2_provider:list" %}"{% endblock app-form-back-url %}
|
||||
@@ -0,0 +1,40 @@
|
||||
{% extends "oauth2_provider/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% block content %}
|
||||
<div class="block-center">
|
||||
{% if not error %}
|
||||
<form id="authorizationForm" method="post">
|
||||
<h3 class="block-center-heading">{% trans "Authorize" %} {{ application.name }}?</h3>
|
||||
{% csrf_token %}
|
||||
|
||||
{% for field in form %}
|
||||
{% if field.is_hidden %}
|
||||
{{ field }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<p>{% trans "Application requires the following permissions" %}</p>
|
||||
<ul>
|
||||
{% for scope in scopes_descriptions %}
|
||||
<li>{{ scope }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{{ form.errors }}
|
||||
{{ form.non_field_errors }}
|
||||
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<input type="submit" class="btn btn-large" value="{% trans 'Cancel' %}"/>
|
||||
<input type="submit" class="btn btn-large btn-primary" name="allow" value="{% trans 'Authorize' %}"/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% else %}
|
||||
<h2>Error: {{ error.error }}</h2>
|
||||
<p>{{ error.description }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,9 @@
|
||||
{% extends "oauth2_provider/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% block content %}
|
||||
<form action="" method="post">{% csrf_token %}
|
||||
<p>{% trans "Are you sure you want to delete this token?" %}</p>
|
||||
<input type="submit" value="{% trans 'Delete' %}" />
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,23 @@
|
||||
{% extends "oauth2_provider/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% block content %}
|
||||
<div class="block-center">
|
||||
<h1>{% trans "Tokens" %}</h1>
|
||||
<ul>
|
||||
{% for authorized_token in authorized_tokens %}
|
||||
<li>
|
||||
{{ authorized_token.application }}
|
||||
(<a href="{% url 'oauth2_provider:authorized-token-delete' authorized_token.pk %}">{% trans "revoke" %}</a>)
|
||||
</li>
|
||||
<ul>
|
||||
{% for scope_name, scope_description in authorized_token.scopes.items %}
|
||||
<li>{{ scope_name }}: {{ scope_description }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% empty %}
|
||||
<li>{% trans "There are no authorized tokens yet." %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,48 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{% block title %}{% endblock title %}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
|
||||
{% block css %}
|
||||
<link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.no-icons.min.css" rel="stylesheet">
|
||||
{% endblock css %}
|
||||
|
||||
<style>
|
||||
body {
|
||||
padding-top: 40px;
|
||||
padding-bottom: 40px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.block-center {
|
||||
max-width: 500px;
|
||||
padding: 19px 29px 29px;
|
||||
margin: 0 auto 20px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #e5e5e5;
|
||||
-webkit-border-radius: 5px;
|
||||
-moz-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
-webkit-box-shadow: 0 1px 2px rgba(0,0,0,.05);
|
||||
-moz-box-shadow: 0 1px 2px rgba(0,0,0,.05);
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,.05);
|
||||
}
|
||||
.block-center .block-center-heading {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
{% block content %}
|
||||
{% endblock content %}
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,37 @@
|
||||
{% extends "oauth2_provider/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% block content %}
|
||||
<div class="block-center">
|
||||
{% if not error %}
|
||||
<form id="authorizationForm" method="post">
|
||||
{% if application %}
|
||||
<h3 class="block-center-heading">Confirm Logout requested by {{ application.name }}</h3>
|
||||
{% else %}
|
||||
<h3 class="block-center-heading">Confirm Logout</h3>
|
||||
{% endif %}
|
||||
{% csrf_token %}
|
||||
|
||||
{% for field in form %}
|
||||
{% if field.is_hidden %}
|
||||
{{ field }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{{ form.errors }}
|
||||
{{ form.non_field_errors }}
|
||||
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<input type="submit" class="btn btn-large" value="Cancel"/>
|
||||
<input type="submit" class="btn btn-large btn-primary" name="allow" value="Logout"/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% else %}
|
||||
<h2>Error: {{ error.error }}</h2>
|
||||
<p>{{ error.description }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
49
venv/lib/python3.11/site-packages/oauth2_provider/urls.py
Normal file
49
venv/lib/python3.11/site-packages/oauth2_provider/urls.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from django.urls import path, re_path
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
app_name = "oauth2_provider"
|
||||
|
||||
|
||||
base_urlpatterns = [
|
||||
path("authorize/", views.AuthorizationView.as_view(), name="authorize"),
|
||||
path("token/", views.TokenView.as_view(), name="token"),
|
||||
path("revoke_token/", views.RevokeTokenView.as_view(), name="revoke-token"),
|
||||
path("introspect/", views.IntrospectTokenView.as_view(), name="introspect"),
|
||||
]
|
||||
|
||||
|
||||
management_urlpatterns = [
|
||||
# Application management views
|
||||
path("applications/", views.ApplicationList.as_view(), name="list"),
|
||||
path("applications/register/", views.ApplicationRegistration.as_view(), name="register"),
|
||||
path("applications/<slug:pk>/", views.ApplicationDetail.as_view(), name="detail"),
|
||||
path("applications/<slug:pk>/delete/", views.ApplicationDelete.as_view(), name="delete"),
|
||||
path("applications/<slug:pk>/update/", views.ApplicationUpdate.as_view(), name="update"),
|
||||
# Token management views
|
||||
path("authorized_tokens/", views.AuthorizedTokensListView.as_view(), name="authorized-token-list"),
|
||||
path(
|
||||
"authorized_tokens/<slug:pk>/delete/",
|
||||
views.AuthorizedTokenDeleteView.as_view(),
|
||||
name="authorized-token-delete",
|
||||
),
|
||||
]
|
||||
|
||||
oidc_urlpatterns = [
|
||||
# .well-known/openid-configuration/ is deprecated
|
||||
# https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig
|
||||
# does not specify a trailing slash
|
||||
# Support for trailing slash shall be removed in a future release.
|
||||
re_path(
|
||||
r"^\.well-known/openid-configuration/?$",
|
||||
views.ConnectDiscoveryInfoView.as_view(),
|
||||
name="oidc-connect-discovery-info",
|
||||
),
|
||||
path(".well-known/jwks.json", views.JwksInfoView.as_view(), name="jwks-info"),
|
||||
path("userinfo/", views.UserInfoView.as_view(), name="user-info"),
|
||||
path("logout/", views.RPInitiatedLogoutView.as_view(), name="rp-initiated-logout"),
|
||||
]
|
||||
|
||||
|
||||
urlpatterns = base_urlpatterns + management_urlpatterns + oidc_urlpatterns
|
||||
34
venv/lib/python3.11/site-packages/oauth2_provider/utils.py
Normal file
34
venv/lib/python3.11/site-packages/oauth2_provider/utils.py
Normal file
@@ -0,0 +1,34 @@
|
||||
import functools
|
||||
|
||||
from django.conf import settings
|
||||
from jwcrypto import jwk
|
||||
|
||||
|
||||
@functools.lru_cache()
|
||||
def jwk_from_pem(pem_string):
|
||||
"""
|
||||
A cached version of jwcrypto.JWK.from_pem.
|
||||
Converting from PEM is expensive for large keys such as those using RSA.
|
||||
"""
|
||||
return jwk.JWK.from_pem(pem_string.encode("utf-8"))
|
||||
|
||||
|
||||
# @functools.lru_cache
|
||||
def get_timezone(time_zone):
|
||||
"""
|
||||
Return the default time zone as a tzinfo instance.
|
||||
|
||||
This is the time zone defined by settings.TIME_ZONE.
|
||||
"""
|
||||
try:
|
||||
import zoneinfo
|
||||
except ImportError:
|
||||
import pytz
|
||||
|
||||
return pytz.timezone(time_zone)
|
||||
else:
|
||||
if getattr(settings, "USE_DEPRECATED_PYTZ", False):
|
||||
import pytz
|
||||
|
||||
return pytz.timezone(time_zone)
|
||||
return zoneinfo.ZoneInfo(time_zone)
|
||||
@@ -0,0 +1,77 @@
|
||||
import re
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import URLValidator
|
||||
from django.utils.encoding import force_str
|
||||
|
||||
|
||||
class URIValidator(URLValidator):
|
||||
scheme_re = r"^(?:[a-z][a-z0-9\.\-\+]*)://"
|
||||
|
||||
dotless_domain_re = r"(?!-)[A-Z\d-]{1,63}(?<!-)"
|
||||
host_re = "|".join(
|
||||
(r"(?:" + URLValidator.host_re, URLValidator.ipv4_re, URLValidator.ipv6_re, dotless_domain_re + ")")
|
||||
)
|
||||
port_re = r"(?::\d{2,5})?"
|
||||
path_re = r"(?:[/?#][^\s]*)?"
|
||||
regex = re.compile(scheme_re + host_re + port_re + path_re, re.IGNORECASE)
|
||||
|
||||
|
||||
class AllowedURIValidator(URIValidator):
|
||||
# TODO: find a way to get these associated with their form fields in place of passing name
|
||||
# TODO: submit PR to get `cause` included in the parent class ValidationError params`
|
||||
def __init__(self, schemes, name, allow_path=False, allow_query=False, allow_fragments=False):
|
||||
"""
|
||||
:param schemes: List of allowed schemes. E.g.: ["https"]
|
||||
:param name: Name of the validated URI. It is required for validation message. E.g.: "Origin"
|
||||
:param allow_path: If URI can contain path part
|
||||
:param allow_query: If URI can contain query part
|
||||
:param allow_fragments: If URI can contain fragments part
|
||||
"""
|
||||
super().__init__(schemes=schemes)
|
||||
self.name = name
|
||||
self.allow_path = allow_path
|
||||
self.allow_query = allow_query
|
||||
self.allow_fragments = allow_fragments
|
||||
|
||||
def __call__(self, value):
|
||||
value = force_str(value)
|
||||
try:
|
||||
scheme, netloc, path, query, fragment = urlsplit(value)
|
||||
except ValueError as e:
|
||||
raise ValidationError(
|
||||
"%(name)s URI validation error. %(cause)s: %(value)s",
|
||||
params={"name": self.name, "value": value, "cause": e},
|
||||
)
|
||||
|
||||
# send better validation errors
|
||||
if scheme not in self.schemes:
|
||||
raise ValidationError(
|
||||
"%(name)s URI Validation error. %(cause)s: %(value)s",
|
||||
params={"name": self.name, "value": value, "cause": "invalid_scheme"},
|
||||
)
|
||||
|
||||
if query and not self.allow_query:
|
||||
raise ValidationError(
|
||||
"%(name)s URI validation error. %(cause)s: %(value)s",
|
||||
params={"name": self.name, "value": value, "cause": "query string not allowed"},
|
||||
)
|
||||
if fragment and not self.allow_fragments:
|
||||
raise ValidationError(
|
||||
"%(name)s URI validation error. %(cause)s: %(value)s",
|
||||
params={"name": self.name, "value": value, "cause": "fragment not allowed"},
|
||||
)
|
||||
if path and not self.allow_path:
|
||||
raise ValidationError(
|
||||
"%(name)s URI validation error. %(cause)s: %(value)s",
|
||||
params={"name": self.name, "value": value, "cause": "path not allowed"},
|
||||
)
|
||||
|
||||
try:
|
||||
super().__call__(value)
|
||||
except ValidationError as e:
|
||||
raise ValidationError(
|
||||
"%(name)s URI validation error. %(cause)s: %(value)s",
|
||||
params={"name": self.name, "value": value, "cause": e},
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
# flake8: noqa
|
||||
from .base import AuthorizationView, TokenView, RevokeTokenView # isort:skip
|
||||
from .application import (
|
||||
ApplicationDelete,
|
||||
ApplicationDetail,
|
||||
ApplicationList,
|
||||
ApplicationRegistration,
|
||||
ApplicationUpdate,
|
||||
)
|
||||
from .generic import (
|
||||
ClientProtectedResourceView,
|
||||
ClientProtectedScopedResourceView,
|
||||
ProtectedResourceView,
|
||||
ReadWriteScopedResourceView,
|
||||
ScopedProtectedResourceView,
|
||||
)
|
||||
from .introspect import IntrospectTokenView
|
||||
from .oidc import ConnectDiscoveryInfoView, JwksInfoView, RPInitiatedLogoutView, UserInfoView
|
||||
from .token import AuthorizedTokenDeleteView, AuthorizedTokensListView
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user