Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add authentication provider for zju #157

Merged
merged 4 commits into from
Oct 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
Dismissed Show dismissed Hide dismissed
{% 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
Loading