This commit is contained in:
Iliyan Angelov
2025-09-14 23:24:25 +03:00
commit c67067a2a4
71311 changed files with 6800714 additions and 0 deletions

View File

@@ -0,0 +1,102 @@
import requests
from urllib.parse import parse_qsl
from django.utils.http import urlencode
class OAuth2Error(Exception):
pass
class OAuth2Client(object):
def __init__(
self,
request,
consumer_key,
consumer_secret,
access_token_method,
access_token_url,
callback_url,
scope,
scope_delimiter=" ",
headers=None,
basic_auth=False,
):
self.request = request
self.access_token_method = access_token_method
self.access_token_url = access_token_url
self.callback_url = callback_url
self.consumer_key = consumer_key
self.consumer_secret = consumer_secret
self.scope = scope_delimiter.join(set(scope))
self.state = None
self.headers = headers
self.basic_auth = basic_auth
def get_redirect_url(self, authorization_url, extra_params):
params = {
"client_id": self.consumer_key,
"redirect_uri": self.callback_url,
"scope": self.scope,
"response_type": "code",
}
if self.state:
params["state"] = self.state
params.update(extra_params)
return "%s?%s" % (authorization_url, urlencode(params))
def get_access_token(self, code, pkce_code_verifier=None):
data = {
"redirect_uri": self.callback_url,
"grant_type": "authorization_code",
"code": code,
}
if self.basic_auth:
auth = requests.auth.HTTPBasicAuth(self.consumer_key, self.consumer_secret)
else:
auth = None
data.update(
{
"client_id": self.consumer_key,
"client_secret": self.consumer_secret,
}
)
params = None
self._strip_empty_keys(data)
url = self.access_token_url
if self.access_token_method == "GET":
params = data
data = None
if data and pkce_code_verifier:
data["code_verifier"] = pkce_code_verifier
# TODO: Proper exception handling
resp = requests.request(
self.access_token_method,
url,
params=params,
data=data,
headers=self.headers,
auth=auth,
)
access_token = None
if resp.status_code in [200, 201]:
# Weibo sends json via 'text/plain;charset=UTF-8'
if (
resp.headers["content-type"].split(";")[0] == "application/json"
or resp.text[:2] == '{"'
):
access_token = resp.json()
else:
access_token = dict(parse_qsl(resp.text))
if not access_token or "access_token" not in access_token:
raise OAuth2Error("Error retrieving access token: %s" % resp.content)
return access_token
def _strip_empty_keys(self, params):
"""Added because the Dropbox OAuth2 flow doesn't
work when scope is passed in, which is empty.
"""
keys = [k for k, v in params.items() if v == ""]
for key in keys:
del params[key]

View File

@@ -0,0 +1,47 @@
from urllib.parse import parse_qsl
from django.urls import reverse
from django.utils.http import urlencode
from allauth.socialaccount.providers.base import Provider
from .utils import generate_code_challenge
class OAuth2Provider(Provider):
pkce_enabled_default = False
def get_login_url(self, request, **kwargs):
url = reverse(self.id + "_login")
if kwargs:
url = url + "?" + urlencode(kwargs)
return url
def get_callback_url(self):
return reverse(self.id + "_callback")
def get_pkce_params(self):
settings = self.get_settings()
if settings.get("OAUTH_PKCE_ENABLED", self.pkce_enabled_default):
pkce_code_params = generate_code_challenge()
return pkce_code_params
return {}
def get_auth_params(self, request, action):
settings = self.get_settings()
ret = dict(settings.get("AUTH_PARAMS", {}))
dynamic_auth_params = request.GET.get("auth_params", None)
if dynamic_auth_params:
ret.update(dict(parse_qsl(dynamic_auth_params)))
return ret
def get_scope(self, request):
settings = self.get_settings()
scope = list(settings.get("SCOPE", self.get_default_scope()))
dynamic_scope = request.GET.get("scope", None)
if dynamic_scope:
scope.extend(dynamic_scope.split(","))
return scope
def get_default_scope(self):
return []

View File

@@ -0,0 +1,15 @@
from django.urls import include, path
from allauth.utils import import_attribute
def default_urlpatterns(provider):
login_view = import_attribute(provider.get_package() + ".views.oauth2_login")
callback_view = import_attribute(provider.get_package() + ".views.oauth2_callback")
urlpatterns = [
path("login/", login_view, name=provider.id + "_login"),
path("login/callback/", callback_view, name=provider.id + "_callback"),
]
return [path(provider.get_slug() + "/", include(urlpatterns))]

View File

@@ -0,0 +1,28 @@
import base64
import hashlib
try:
from secrets import token_urlsafe
except ImportError:
# token_urlsafe polyfill for Python < 3.6
import os
def token_urlsafe(nbytes=None):
if nbytes is None:
nbytes = 32
tok = os.urandom(nbytes)
return base64.urlsafe_b64encode(tok).rstrip(b"=").decode("ascii")
def generate_code_challenge():
# Create a code verifier with a length of 128 characters
code_verifier = token_urlsafe(96)
hashed_verifier = hashlib.sha256(code_verifier.encode("ascii"))
code_challenge = base64.urlsafe_b64encode(hashed_verifier.digest())
code_challenge_without_padding = code_challenge.rstrip(b"=")
return {
"code_verifier": code_verifier,
"code_challenge_method": "S256",
"code_challenge": code_challenge_without_padding,
}

View File

@@ -0,0 +1,173 @@
from __future__ import absolute_import
from datetime import timedelta
from requests import RequestException
from django.core.exceptions import PermissionDenied
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.utils import timezone
from allauth.core.exceptions import ImmediateHttpResponse
from allauth.socialaccount.adapter import get_adapter
from allauth.socialaccount.helpers import (
complete_social_login,
render_authentication_error,
)
from allauth.socialaccount.models import SocialLogin, SocialToken
from allauth.socialaccount.providers.base import ProviderException
from allauth.socialaccount.providers.base.constants import (
AuthAction,
AuthError,
)
from allauth.socialaccount.providers.base.mixins import OAuthLoginMixin
from allauth.socialaccount.providers.oauth2.client import (
OAuth2Client,
OAuth2Error,
)
from allauth.utils import build_absolute_uri, get_request_param
class OAuth2Adapter(object):
expires_in_key = "expires_in"
client_class = OAuth2Client
supports_state = True
redirect_uri_protocol = None
access_token_method = "POST"
login_cancelled_error = "access_denied"
scope_delimiter = " "
basic_auth = False
headers = None
def __init__(self, request):
self.request = request
def get_provider(self):
return get_adapter(self.request).get_provider(
self.request, provider=self.provider_id
)
def complete_login(self, request, app, access_token, **kwargs):
"""
Returns a SocialLogin instance
"""
raise NotImplementedError
def get_callback_url(self, request, app):
callback_url = reverse(self.provider_id + "_callback")
protocol = self.redirect_uri_protocol
return build_absolute_uri(request, callback_url, protocol)
def parse_token(self, data):
token = SocialToken(token=data["access_token"])
token.token_secret = data.get("refresh_token", "")
expires_in = data.get(self.expires_in_key, None)
if expires_in:
token.expires_at = timezone.now() + timedelta(seconds=int(expires_in))
return token
def get_access_token_data(self, request, app, client):
code = get_request_param(self.request, "code")
pkce_code_verifier = request.session.pop("pkce_code_verifier", None)
return client.get_access_token(code, pkce_code_verifier=pkce_code_verifier)
class OAuth2View(object):
@classmethod
def adapter_view(cls, adapter):
def view(request, *args, **kwargs):
self = cls()
self.request = request
if not isinstance(adapter, OAuth2Adapter):
self.adapter = adapter(request)
else:
self.adapter = adapter
try:
return self.dispatch(request, *args, **kwargs)
except ImmediateHttpResponse as e:
return e.response
return view
def get_client(self, request, app):
callback_url = self.adapter.get_callback_url(request, app)
provider = self.adapter.get_provider()
scope = provider.get_scope(request)
client = self.adapter.client_class(
self.request,
app.client_id,
app.secret,
self.adapter.access_token_method,
self.adapter.access_token_url,
callback_url,
scope,
scope_delimiter=self.adapter.scope_delimiter,
headers=self.adapter.headers,
basic_auth=self.adapter.basic_auth,
)
return client
class OAuth2LoginView(OAuthLoginMixin, OAuth2View):
def login(self, request, *args, **kwargs):
provider = self.adapter.get_provider()
app = provider.app
client = self.get_client(request, app)
action = request.GET.get("action", AuthAction.AUTHENTICATE)
auth_url = self.adapter.authorize_url
auth_params = provider.get_auth_params(request, action)
pkce_params = provider.get_pkce_params()
code_verifier = pkce_params.pop("code_verifier", None)
auth_params.update(pkce_params)
if code_verifier:
request.session["pkce_code_verifier"] = code_verifier
client.state = SocialLogin.stash_state(request)
try:
return HttpResponseRedirect(client.get_redirect_url(auth_url, auth_params))
except OAuth2Error as e:
return render_authentication_error(request, provider.id, exception=e)
class OAuth2CallbackView(OAuth2View):
def dispatch(self, request, *args, **kwargs):
if "error" in request.GET or "code" not in request.GET:
# Distinguish cancel from error
auth_error = request.GET.get("error", None)
if auth_error == self.adapter.login_cancelled_error:
error = AuthError.CANCELLED
else:
error = AuthError.UNKNOWN
return render_authentication_error(
request, self.adapter.provider_id, error=error
)
app = self.adapter.get_provider().app
client = self.get_client(self.request, app)
try:
access_token = self.adapter.get_access_token_data(request, app, client)
token = self.adapter.parse_token(access_token)
if app.pk:
token.app = app
login = self.adapter.complete_login(
request, app, token, response=access_token
)
login.token = token
if self.adapter.supports_state:
login.state = SocialLogin.verify_and_unstash_state(
request, get_request_param(request, "state")
)
else:
login.state = SocialLogin.unstash_state(request)
return complete_social_login(request, login)
except (
PermissionDenied,
OAuth2Error,
RequestException,
ProviderException,
) as e:
return render_authentication_error(
request, self.adapter.provider_id, exception=e
)