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,15 @@
from django.contrib import admin
from .models import OpenIDNonce, OpenIDStore
class OpenIDStoreAdmin(admin.ModelAdmin):
pass
class OpenIDNonceAdmin(admin.ModelAdmin):
pass
admin.site.register(OpenIDStore, OpenIDStoreAdmin)
admin.site.register(OpenIDNonce, OpenIDNonceAdmin)

View File

@@ -0,0 +1,10 @@
from django import forms
class LoginForm(forms.Form):
openid = forms.URLField(
label=("OpenID"),
help_text=('Get an <a href="http://openidexplained.com/get">OpenID</a>'),
)
next = forms.CharField(widget=forms.HiddenInput, required=False)
process = forms.CharField(widget=forms.HiddenInput, required=False)

View File

@@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = []
operations = [
migrations.CreateModel(
name="OpenIDNonce",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("server_url", models.CharField(max_length=255)),
("timestamp", models.IntegerField()),
("salt", models.CharField(max_length=255)),
("date_created", models.DateTimeField(auto_now_add=True)),
],
options={},
bases=(models.Model,),
),
migrations.CreateModel(
name="OpenIDStore",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("server_url", models.CharField(max_length=255)),
("handle", models.CharField(max_length=255)),
("secret", models.TextField()),
("issued", models.IntegerField()),
("lifetime", models.IntegerField()),
("assoc_type", models.TextField()),
],
options={},
bases=(models.Model,),
),
]

View File

@@ -0,0 +1,23 @@
from django.db import models
class OpenIDStore(models.Model):
server_url = models.CharField(max_length=255)
handle = models.CharField(max_length=255)
secret = models.TextField()
issued = models.IntegerField()
lifetime = models.IntegerField()
assoc_type = models.TextField()
def __str__(self):
return self.server_url
class OpenIDNonce(models.Model):
server_url = models.CharField(max_length=255)
timestamp = models.IntegerField()
salt = models.CharField(max_length=255)
date_created = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.server_url

View File

@@ -0,0 +1,115 @@
from urllib.parse import urlparse
from django.urls import reverse
from django.utils.http import urlencode
from allauth.socialaccount.providers.base import Provider, ProviderAccount
from .utils import (
AXAttribute,
OldAXAttribute,
SRegField,
get_email_from_response,
get_value_from_response,
)
class OpenIDAccount(ProviderAccount):
def get_brand(self):
ret = super(OpenIDAccount, self).get_brand()
domain = urlparse(self.account.uid).netloc
# FIXME: Instead of hardcoding, derive this from the domains
# listed in the openid endpoints setting.
provider_map = {
"yahoo": dict(id="yahoo", name="Yahoo"),
"hyves": dict(id="hyves", name="Hyves"),
"google": dict(id="google", name="Google"),
}
for d, p in provider_map.items():
if domain.lower().find(d) >= 0:
ret = p
break
return ret
def to_str(self):
return self.account.uid
class OpenIDProvider(Provider):
id = "openid"
name = "OpenID"
account_class = OpenIDAccount
uses_apps = False
def get_login_url(self, request, **kwargs):
url = reverse("openid_login")
if kwargs:
url += "?" + urlencode(kwargs)
return url
def get_brands(self):
# These defaults are a bit too arbitrary...
default_servers = [
dict(id="yahoo", name="Yahoo", openid_url="http://me.yahoo.com"),
dict(id="hyves", name="Hyves", openid_url="http://hyves.nl"),
]
return self.get_settings().get("SERVERS", default_servers)
def get_server_settings(self, endpoint):
servers = self.get_settings().get("SERVERS", [])
for server in servers:
if endpoint is not None and endpoint.startswith(server.get("openid_url")):
return server
return {}
def extract_extra_data(self, response):
extra_data = {}
server_settings = self.get_server_settings(response.endpoint.server_url)
extra_attributes = server_settings.get("extra_attributes", [])
for attribute_id, name, _ in extra_attributes:
extra_data[attribute_id] = get_value_from_response(
response, ax_names=[name]
)
return extra_data
def extract_uid(self, response):
return response.identity_url
def extract_common_fields(self, response):
first_name = (
get_value_from_response(
response,
ax_names=[
AXAttribute.PERSON_FIRST_NAME,
OldAXAttribute.PERSON_FIRST_NAME,
],
)
or ""
)
last_name = (
get_value_from_response(
response,
ax_names=[
AXAttribute.PERSON_LAST_NAME,
OldAXAttribute.PERSON_LAST_NAME,
],
)
or ""
)
name = (
get_value_from_response(
response,
sreg_names=[SRegField.NAME],
ax_names=[AXAttribute.PERSON_NAME, OldAXAttribute.PERSON_NAME],
)
or ""
)
return dict(
email=get_email_from_response(response),
first_name=first_name,
last_name=last_name,
name=name,
)
provider_classes = [OpenIDProvider]

View File

@@ -0,0 +1,132 @@
from unittest import expectedFailure
from django.test import override_settings
from django.urls import reverse
from openid.consumer import consumer
from allauth.socialaccount.models import SocialAccount
from allauth.tests import Mock, TestCase, patch
from allauth.utils import get_user_model
from . import views
from .utils import AXAttribute
class OpenIDTests(TestCase):
def test_discovery_failure(self):
"""
This used to generate a server 500:
DiscoveryFailure: No usable OpenID services found
for http://www.google.com/
"""
resp = self.client.post(
reverse("openid_login"), dict(openid="http://www.google.com")
)
self.assertTrue("openid" in resp.context["form"].errors)
@expectedFailure
def test_login(self):
# Location: https://s.yimg.com/wm/mbr/html/openid-eol-0.0.1.html
resp = self.client.post(
reverse(views.login), dict(openid="http://me.yahoo.com")
)
assert "login.yahooapis" in resp["location"]
with patch(
"allauth.socialaccount.providers.openid.views._openid_consumer"
) as consumer_mock:
client = Mock()
complete = Mock()
consumer_mock.return_value = client
client.complete = complete
complete_response = Mock()
complete.return_value = complete_response
complete_response.status = consumer.SUCCESS
complete_response.identity_url = "http://dummy/john/"
with patch(
"allauth.socialaccount.providers.openid.utils.SRegResponse"
) as sr_mock:
with patch(
"allauth.socialaccount.providers.openid.utils.FetchResponse"
) as fr_mock:
sreg_mock = Mock()
ax_mock = Mock()
sr_mock.fromSuccessResponse = sreg_mock
fr_mock.fromSuccessResponse = ax_mock
sreg_mock.return_value = {}
ax_mock.return_value = {AXAttribute.PERSON_FIRST_NAME: ["raymond"]}
resp = self.client.post(reverse("openid_callback"))
self.assertRedirects(
resp,
"/accounts/profile/",
fetch_redirect_response=False,
)
get_user_model().objects.get(first_name="raymond")
@expectedFailure
@override_settings(
SOCIALACCOUNT_PROVIDERS={
"openid": {
"SERVERS": [
dict(
id="yahoo",
name="Yahoo",
openid_url="http://me.yahoo.com",
extra_attributes=[
(
"phone",
"http://axschema.org/contact/phone/default",
True,
)
],
)
]
}
}
)
def test_login_with_extra_attributes(self):
with patch("allauth.socialaccount.providers.openid.views.QUERY_EMAIL", True):
resp = self.client.post(
reverse(views.login), dict(openid="http://me.yahoo.com")
)
assert "login.yahooapis" in resp["location"]
with patch(
"allauth.socialaccount.providers.openid.views._openid_consumer"
) as consumer_mock:
client = Mock()
complete = Mock()
endpoint = Mock()
consumer_mock.return_value = client
client.complete = complete
complete_response = Mock()
complete.return_value = complete_response
complete_response.endpoint = endpoint
complete_response.endpoint.server_url = "http://me.yahoo.com"
complete_response.status = consumer.SUCCESS
complete_response.identity_url = "http://dummy/john/"
with patch(
"allauth.socialaccount.providers.openid.utils.SRegResponse"
) as sr_mock:
with patch(
"allauth.socialaccount.providers.openid.utils.FetchResponse"
) as fr_mock:
sreg_mock = Mock()
ax_mock = Mock()
sr_mock.fromSuccessResponse = sreg_mock
fr_mock.fromSuccessResponse = ax_mock
sreg_mock.return_value = {}
ax_mock.return_value = {
AXAttribute.CONTACT_EMAIL: ["raymond@example.com"],
AXAttribute.PERSON_FIRST_NAME: ["raymond"],
"http://axschema.org/contact/phone/default": ["123456789"],
}
resp = self.client.post(reverse("openid_callback"))
self.assertRedirects(
resp,
"/accounts/profile/",
fetch_redirect_response=False,
)
socialaccount = SocialAccount.objects.get(
user__first_name="raymond"
)
self.assertEqual(socialaccount.extra_data.get("phone"), "123456789")

View File

@@ -0,0 +1,9 @@
from django.urls import path
from . import views
urlpatterns = [
path("openid/login/", views.login, name="openid_login"),
path("openid/callback/", views.callback, name="openid_callback"),
]

View File

@@ -0,0 +1,184 @@
import base64
import pickle
from collections import UserDict
from openid.association import Association as OIDAssociation
from openid.extensions.ax import FetchResponse
from openid.extensions.sreg import SRegResponse
from openid.store.interface import OpenIDStore as OIDStore
from allauth.utils import valid_email_or_none
from .models import OpenIDNonce, OpenIDStore
class JSONSafeSession(UserDict):
"""
openid puts e.g. class OpenIDServiceEndpoint in the session.
Django 1.6 no longer pickles stuff, so we'll need to do some
hacking here...
"""
def __init__(self, session):
UserDict.__init__(self)
self.data = session
def __setitem__(self, key, value):
data = base64.b64encode(pickle.dumps(value)).decode("ascii")
return UserDict.__setitem__(self, key, data)
def __getitem__(self, key):
data = UserDict.__getitem__(self, key)
return pickle.loads(base64.b64decode(data.encode("ascii")))
class OldAXAttribute:
PERSON_NAME = "http://openid.net/schema/namePerson"
PERSON_FIRST_NAME = "http://openid.net/schema/namePerson/first"
PERSON_LAST_NAME = "http://openid.net/schema/namePerson/last"
class AXAttribute:
CONTACT_EMAIL = "http://axschema.org/contact/email"
PERSON_NAME = "http://axschema.org/namePerson"
PERSON_FIRST_NAME = "http://axschema.org/namePerson/first"
PERSON_LAST_NAME = "http://axschema.org/namePerson/last"
AXAttributes = [
AXAttribute.CONTACT_EMAIL,
AXAttribute.PERSON_NAME,
AXAttribute.PERSON_FIRST_NAME,
AXAttribute.PERSON_LAST_NAME,
OldAXAttribute.PERSON_NAME,
OldAXAttribute.PERSON_FIRST_NAME,
OldAXAttribute.PERSON_LAST_NAME,
]
class SRegField:
EMAIL = "email"
NAME = "fullname"
SRegFields = [
SRegField.EMAIL,
SRegField.NAME,
]
class DBOpenIDStore(OIDStore):
max_nonce_age = 6 * 60 * 60
def storeAssociation(self, server_url, assoc=None):
try:
secret = base64.encodebytes(assoc.secret)
except AttributeError:
# Python 2.x compat
secret = base64.encodestring(assoc.secret)
else:
secret = secret.decode()
OpenIDStore.objects.create(
server_url=server_url,
handle=assoc.handle,
secret=secret,
issued=assoc.issued,
lifetime=assoc.lifetime,
assoc_type=assoc.assoc_type,
)
def getAssociation(self, server_url, handle=None):
stored_assocs = OpenIDStore.objects.filter(server_url=server_url)
if handle:
stored_assocs = stored_assocs.filter(handle=handle)
stored_assocs.order_by("-issued")
if not stored_assocs.exists():
return None
return_val = None
for stored_assoc in stored_assocs:
assoc = OIDAssociation(
stored_assoc.handle,
base64.decodebytes(stored_assoc.secret.encode("utf-8")),
stored_assoc.issued,
stored_assoc.lifetime,
stored_assoc.assoc_type,
)
# See:
# necaris/python3-openid@1abb155c8fc7b508241cbe9d2cae24f18e4a379b
if hasattr(assoc, "getExpiresIn"):
expires_in = assoc.getExpiresIn()
else:
expires_in = assoc.expiresIn
if expires_in == 0:
stored_assoc.delete()
else:
if return_val is None:
return_val = assoc
return return_val
def removeAssociation(self, server_url, handle):
stored_assocs = OpenIDStore.objects.filter(server_url=server_url)
if handle:
stored_assocs = stored_assocs.filter(handle=handle)
stored_assocs.delete()
def useNonce(self, server_url, timestamp, salt):
try:
OpenIDNonce.objects.get(
server_url=server_url, timestamp=timestamp, salt=salt
)
except OpenIDNonce.DoesNotExist:
OpenIDNonce.objects.create(
server_url=server_url, timestamp=timestamp, salt=salt
)
return True
return False
def get_email_from_response(response):
email = None
sreg = SRegResponse.fromSuccessResponse(response)
if sreg:
email = valid_email_or_none(sreg.get(SRegField.EMAIL))
if not email:
ax = FetchResponse.fromSuccessResponse(response)
if ax:
try:
values = ax.get(AXAttribute.CONTACT_EMAIL)
if values:
email = valid_email_or_none(values[0])
except KeyError:
pass
return email
def get_value_from_response(response, sreg_names=None, ax_names=None):
value = None
if sreg_names:
sreg = SRegResponse.fromSuccessResponse(response)
if sreg:
for name in sreg_names:
value = sreg.get(name)
if value:
break
if not value and ax_names:
ax = FetchResponse.fromSuccessResponse(response)
if ax:
for name in ax_names:
try:
values = ax.get(name)
if values:
value = values[0]
except KeyError:
pass
if value:
break
return value

View File

@@ -0,0 +1,161 @@
from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.urls import reverse
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from openid.consumer import consumer
from openid.consumer.discover import DiscoveryFailure
from openid.extensions.ax import AttrInfo, FetchRequest
from openid.extensions.sreg import SRegRequest
from allauth.socialaccount.app_settings import QUERY_EMAIL
from allauth.socialaccount.helpers import (
complete_social_login,
render_authentication_error,
)
from allauth.socialaccount.models import SocialLogin
from ..base import AuthError
from .forms import LoginForm
from .provider import OpenIDProvider
from .utils import AXAttributes, DBOpenIDStore, JSONSafeSession, SRegFields
def _openid_consumer(request, provider, endpoint):
server_settings = provider.get_server_settings(endpoint)
stateless = server_settings.get("stateless", False)
store = None if stateless else DBOpenIDStore()
client = consumer.Consumer(JSONSafeSession(request.session), store)
return client
class OpenIDLoginView(View):
template_name = "openid/login.html"
form_class = LoginForm
provider = OpenIDProvider
def get(self, request):
form = self.get_form()
if not form.is_valid():
return render(request, self.template_name, {"form": form})
try:
return self.perform_openid_auth(form)
except (UnicodeDecodeError, DiscoveryFailure) as e:
# UnicodeDecodeError: necaris/python3-openid#1
return render_authentication_error(request, self.provider.id, exception=e)
def post(self, request):
form = self.get_form()
if form.is_valid():
try:
return self.perform_openid_auth(form)
except (UnicodeDecodeError, DiscoveryFailure) as e:
form._errors["openid"] = form.error_class([e])
return render(request, self.template_name, {"form": form})
def get_form(self):
if self.request.method == "GET" and "openid" not in self.request.GET:
return self.form_class(
initial={
"next": self.request.GET.get("next"),
"process": self.request.GET.get("process"),
}
)
return self.form_class(
dict(list(self.request.GET.items()) + list(self.request.POST.items()))
)
def get_client(self, provider, endpoint):
return _openid_consumer(self.request, provider, endpoint)
def get_realm(self, provider):
return provider.get_settings().get(
"REALM", self.request.build_absolute_uri("/")
)
def get_callback_url(self):
return reverse(callback)
def perform_openid_auth(self, form):
if not form.is_valid():
return form
request = self.request
provider = self.provider(request)
endpoint = form.cleaned_data["openid"]
client = self.get_client(provider, endpoint)
realm = self.get_realm(provider)
auth_request = client.begin(endpoint)
if QUERY_EMAIL:
sreg = SRegRequest()
for name in SRegFields:
sreg.requestField(field_name=name, required=True)
auth_request.addExtension(sreg)
ax = FetchRequest()
for name in AXAttributes:
ax.add(AttrInfo(name, required=True))
provider = OpenIDProvider(request)
server_settings = provider.get_server_settings(request.GET.get("openid"))
extra_attributes = server_settings.get("extra_attributes", [])
for _, name, required in extra_attributes:
ax.add(AttrInfo(name, required=required))
auth_request.addExtension(ax)
SocialLogin.stash_state(request)
# Fix for issues 1523 and 2072 (github django-allauth)
if "next" in form.cleaned_data and form.cleaned_data["next"]:
auth_request.return_to_args["next"] = form.cleaned_data["next"]
redirect_url = auth_request.redirectURL(
realm, request.build_absolute_uri(self.get_callback_url())
)
return HttpResponseRedirect(redirect_url)
login = OpenIDLoginView.as_view()
class OpenIDCallbackView(View):
provider = OpenIDProvider
def get(self, request):
provider = self.provider(request)
endpoint = request.GET.get("openid.op_endpoint", "")
client = self.get_client(provider, endpoint)
response = self.get_openid_response(client)
if response.status == consumer.SUCCESS:
login = provider.sociallogin_from_response(request, response)
login.state = SocialLogin.unstash_state(request)
return self.complete_login(login)
else:
if response.status == consumer.CANCEL:
error = AuthError.CANCELLED
else:
error = AuthError.UNKNOWN
return self.render_error(error)
post = get
def complete_login(self, login):
return complete_social_login(self.request, login)
def render_error(self, error):
return render_authentication_error(self.request, self.provider.id, error=error)
def get_client(self, provider, endpoint):
return _openid_consumer(self.request, provider, endpoint)
def get_openid_response(self, client):
return client.complete(
dict(list(self.request.GET.items()) + list(self.request.POST.items())),
self.request.build_absolute_uri(self.request.path),
)
callback = csrf_exempt(OpenIDCallbackView.as_view())