Skip to content

Commit

Permalink
Merge pull request #157 from ustclug/zju_auth
Browse files Browse the repository at this point in the history
Add authentication provider for zju
  • Loading branch information
RTXUX authored Oct 15, 2023
2 parents 607d6fc + b2b4533 commit 4befe02
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 19 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/smoketest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements-lock.txt
cp conf/local_settings.py.example conf/local_settings.py
PRIVATE_KEY=$(openssl ecparam -name secp256k1 -genkey -noout)
Expand Down
108 changes: 95 additions & 13 deletions frontend/auth_providers/zju.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,106 @@
from datetime import timedelta
import secrets
import string
import json
from typing import Any
from Crypto.Cipher import AES
from base64 import b64decode
from urllib.parse import urlencode

from django.conf import settings
from django.contrib import messages
from django.shortcuts import redirect
from django.template.response import TemplateResponse
from django.urls import path
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt

from .base import DomainEmailValidator
from .external import ExternalLoginView, ExternalGetCodeView
from .base import BaseLoginView


class LoginView(ExternalLoginView):
template_context = {'provider_name': '浙江大学'}
provider = 'zju'
group = 'zju'
class LoginView(BaseLoginView):
template_name = "login_info.html"
template_context = {
"provider_name": "浙江大学统一身份认证",
"info": "此次登录需要您处在一个能够访问浙江大学内网的环境;如果您不在校内,请尝试使用 RVPN 等工具访问内网;确认能访问内网后,请单击登录按钮。",
}
provider = "zju"
group = "zju"

def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
# 避免 local_settings 未配置时出错
provider = settings.EXTERNAL_LOGINS.get(self.provider)
if not provider:
raise RuntimeError(f"未配置{self.template_context['provider_name']}登录")
self.state_key = provider["state_key"].encode()
self.cipher_key = b64decode(provider["cipher_key"])
self.provider_url = provider["provider_url"]

class GetCodeView(ExternalGetCodeView):
provider = 'zju'
duration = timedelta(hours=1)
validate_identity = DomainEmailValidator('zju.edu.cn')
# XXX: POST 请求到外部,不宜外传 csrf token,且反 CSRF 的功能已被 nonce 实现
@method_decorator(csrf_exempt)
def dispatch(self, *args, **kwargs):
return super(LoginView, self).dispatch(*args, **kwargs)

def normalize_identity(self):
return self.identity.casefold()

def check_cipher(self) -> bool:
try:
ciphertext = self.cipher
nonce = self.request.session.get("auth_nonce_zju")
assert isinstance(ciphertext, str) and isinstance(nonce, str)
self.request.session.pop("auth_nonce_zju")
# ciphertext:
# 16 bytes: nonce
# 16 bytes till end - 16 bytes: payload
# 16 bytes from end: MAC
ciphertext = b64decode(ciphertext)
cipher = AES.new(self.cipher_key, AES.MODE_GCM, ciphertext[:16])
cipher.update(nonce.encode())
payload = json.loads(
cipher.decrypt_and_verify(ciphertext[16:-16], ciphertext[-16:])
)
sno = payload["sno"]
assert isinstance(sno, str)
sno = sno.strip()
# XXX: 实际上的长度是 8,但作为可信内容放宽限制来避免一些特殊情况
if not (
all(char in string.digits for char in sno)
and 5 <= len(sno) <= 26
):
messages.error(self.request, "学号非法")
return False
self.sno = sno
self.name = payload["name"]
# XXX: 能够以此学号登录应当与拥有此邮箱等价
self.identity = sno + "@zju.edu.cn"
return True
except Exception:
messages.error(self.request, "登录失败")
return False

def get(self, request):
self.cipher = request.GET.get("cipher")
if self.cipher:
if self.check_cipher():
self.login(email=self.identity, sno=self.sno, name=self.name)
return redirect("hub")
template_context = self.template_context.copy()
nonce = "".join(
secrets.choice(string.ascii_letters + string.digits) for _ in range(32)
)
request.session['auth_nonce_zju'] = nonce

# 在登录前显示提示信息(而非直接跳转)
redirect_uri = request.build_absolute_uri("/accounts/zju/login/")
template_context["url"] = (
self.provider_url
+ "?"
+ urlencode({"redirect_uri": redirect_uri, "nonce": nonce})
)
return TemplateResponse(request, self.template_name, template_context)


urlpatterns = [
path('zju/login/', LoginView.as_view()),
path('zju/get_code/', GetCodeView.as_view()),
path("zju/login/", LoginView.as_view()),
]
15 changes: 15 additions & 0 deletions frontend/templates/login_info.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{% extends 'base.html' %}

{% comment %}包含自定义 info 的登录页模板{% endcomment %}

{% block content %}
{% block form %}
<h1>{% block heading %}{{ provider_name }}登录{% endblock %}</h1>
<button type="submit" onclick="location.href=this.value;" value="{{ url }}" class="pure-button pure-button-primary">登录</button>
{% block info %}
<div class="msg-info">
<p>{{ info }}</p>
</div>
{% endblock %}
{% endblock %}
{% endblock %}
13 changes: 8 additions & 5 deletions requirements-lock.txt
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
aliyun-python-sdk-core==2.13.36
asgiref==3.7.2
certifi==2023.7.22
cffi==1.15.1
charset-normalizer==3.2.0
cffi==1.16.0
charset-normalizer==3.3.0
cryptography==41.0.4
defusedxml==0.7.1
Django==4.2.5
django-allauth==0.57.0
gevent==23.9.1
greenlet==3.0.0rc3
greenlet==3.0.0
idna==3.4
importlib-metadata==6.8.0
jmespath==0.10.0
Markdown==3.4.4
oauthlib==3.2.2
psycopg==3.1.12
psycopg-binary==3.1.12
psycopg-pool==3.1.8
pycparser==2.21
pycryptodome==3.19.0
PyJWT==2.8.0
pymemcache==4.0.0
pyOpenSSL==23.2.0
Expand All @@ -25,8 +27,9 @@ PyYAML==6.0.1
requests==2.31.0
requests-oauthlib==1.3.1
sqlparse==0.4.4
typing_extensions==4.7.1
typing-extensions==4.8.0
urllib3==2.0.6
uWSGI==2.0.22
zipp==3.17.0
zope.event==5.0
zope.interface==6.0
zope.interface==6.1
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Markdown==3.4.4
psycopg==3.1.12
psycopg-binary==3.1.12
psycopg-pool==3.1.8
pycryptodome==3.19.0
pymemcache==4.0.0
pyOpenSSL==23.2.0
PyYAML==6.0.1
Expand Down

0 comments on commit 4befe02

Please sign in to comment.