import requests from urllib.parse import urlencode from django.views.generic import View, FormView, TemplateView from django.urls import reverse from django.http.response import HttpResponse, HttpResponseRedirect from django.utils.crypto import get_random_string from django.conf import settings from django.contrib.auth import login from social_django.models import UserSocialAuth from pingcap_sso.lib.sso import sync_cookie_url from pingcap_sso.models import User from pingcap_sso.forms.sso import ( SocialAuthAddPhoneForm, SocialAuthConfirmBindingExistsAccountForm ) class MPVerifyView(View): def get(self, request, *args, **kwargs): # This view is used to verify the ownership of the domain for WeChat return HttpResponse(content="pdxQqtfOWeIpp4u4") class RequireLoggedOutMixin: def dispatch(self, request, *args, **kwargs): if request.user.is_authenticated: return HttpResponseRedirect(reverse('index')) return super().dispatch(request, *args, **kwargs) WECHAT_PROVIDER = 'wechat' WECHAT_REDIRECT_SESSION_KEY = 'redirect_to' WECHAT_STATE_SESSION_KEY = 'wechat_login_state' WECHAT_OAUTH_OPEN_AUTHORIZE_URL = 'https://open.weixin.qq.com/connect/qrconnect' WECHAT_OAUTH_MP_AUTHORIZE_URL = 'https://open.weixin.qq.com/connect/oauth2/authorize' WECHAT_OAUTH_TOKEN_URL = 'https://api.weixin.qq.com/sns/oauth2/access_token' def is_wechat_browser(request): return 'micromessenger' in request.META.get('HTTP_USER_AGENT', '').lower() class WechatLoginView(RequireLoggedOutMixin, View): def get(self, request, *args, **kwargs): state = get_random_string(32) request.session[WECHAT_REDIRECT_SESSION_KEY] = request.GET.get('redirect_to', reverse('index')) request.session[WECHAT_STATE_SESSION_KEY] = state if is_wechat_browser(request): url_params = { 'appid': settings.WECHAT_MP_APP_ID, 'redirect_uri': request.build_absolute_uri(reverse('wechat-login-callback')), 'response_type': 'code', 'scope': 'snsapi_userinfo', 'state': state } authorize_url = f'{WECHAT_OAUTH_MP_AUTHORIZE_URL}?{urlencode(url_params)}#wechat_redirect' else: url_params = { 'appid': settings.WECHAT_OPEN_APP_ID, 'redirect_uri': request.build_absolute_uri(reverse('wechat-login-callback')), 'response_type': 'code', 'scope': 'snsapi_login', 'state': state } authorize_url = f'{WECHAT_OAUTH_OPEN_AUTHORIZE_URL}?{urlencode(url_params)}#wechat_redirect' return HttpResponseRedirect(authorize_url) class WechatLoginCallbackView(RequireLoggedOutMixin, View): def get(self, request, *args, **kwargs): state = request.session.pop(WECHAT_STATE_SESSION_KEY, None) if not state or state != request.GET.get('state'): return HttpResponseRedirect(reverse('login')) code = request.GET.get('code') if is_wechat_browser(request): app_id = settings.WECHAT_MP_APP_ID app_secret = settings.WECHAT_MP_APP_SECRET else: app_id = settings.WECHAT_OPEN_APP_ID app_secret = settings.WECHAT_OPEN_APP_SECRET url_params = { 'appid': app_id, 'secret': app_secret, 'code': code, 'grant_type': 'authorization_code' } try: resp = requests.get( f'{WECHAT_OAUTH_TOKEN_URL}?{urlencode(url_params)}', timeout=5, ).json() unionid = resp['unionid'] except Exception: return HttpResponse('Somthing wrong, please try again') try: auth = UserSocialAuth.objects.get(provider=WECHAT_PROVIDER, uid=unionid) auth.extra_data = resp auth.save() if auth.user.is_active: login(request, auth.user, backend='pingcap_sso.backends.SimpleModelBackend') return HttpResponseRedirect(sync_cookie_url(request.session.pop(WECHAT_REDIRECT_SESSION_KEY, reverse('index')))) else: return HttpResponse('This account has been disabled, please contact the administrator') except UserSocialAuth.DoesNotExist: set_social_auth_data_session(request, WECHAT_PROVIDER, unionid, resp) return HttpResponseRedirect(reverse('social-auth-add-phone')) SOCIAL_AUTH_BIND_PHONE_SESSION_KEY = 'social_auth_bind_phone' SOCIAL_AUTH_NEW_USER_SESSION_KEY = 'social_auth_new_user' SOCIAL_AUTH_DATA_SESSION_KEY = 'social_auth_data' def is_social_auth_new_user(request): return request.session.get(SOCIAL_AUTH_NEW_USER_SESSION_KEY, False) def clear_social_auth_new_user_session(request): request.session.pop(SOCIAL_AUTH_BIND_PHONE_SESSION_KEY, None) request.session.pop(SOCIAL_AUTH_NEW_USER_SESSION_KEY, None) request.session.pop(SOCIAL_AUTH_DATA_SESSION_KEY, None) def set_social_auth_data_session(request, provider, uid, extra_data): request.session[SOCIAL_AUTH_NEW_USER_SESSION_KEY] = True request.session[SOCIAL_AUTH_DATA_SESSION_KEY] = { 'provider': provider, 'uid': uid, 'extra_data': extra_data } def get_social_auth_data_session(request): data = request.session.get(SOCIAL_AUTH_DATA_SESSION_KEY) return data['provider'], data['uid'], data['extra_data'] class SocialAuthAddPhoneView(RequireLoggedOutMixin, FormView): template_name = 'pingcap_sso/update_phone.html' form_class = SocialAuthAddPhoneForm def dispatch(self, request, *args, **kwargs): if not is_social_auth_new_user(request): return HttpResponseRedirect(reverse('index')) return super().dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): kwargs = super().get_context_data(**kwargs) redirect_to = self.request.session.get(WECHAT_REDIRECT_SESSION_KEY, reverse('index')) kwargs['logout_url'] = f'{reverse("logout")}?redirect_to={redirect_to}' return kwargs def form_valid(self, form): if form.phone_already_used: self.request.session[SOCIAL_AUTH_BIND_PHONE_SESSION_KEY] = form.cleaned_data.get('phone') return HttpResponseRedirect(reverse('social-auth-cofirm-bindding-exists-account')) else: provider, uid, extra_data = get_social_auth_data_session(self.request) user = form.create_social_auth_user(provider, uid, extra_data, self.request.session.get(WECHAT_REDIRECT_SESSION_KEY, '')) clear_social_auth_new_user_session(self.request) login(self.request, user, backend='pingcap_sso.backends.SimpleModelBackend') return HttpResponseRedirect(sync_cookie_url(self.request.session.pop(WECHAT_REDIRECT_SESSION_KEY, reverse('index')))) class SocialAuthConfirmBindingExistsAccountView(RequireLoggedOutMixin, FormView): template_name = 'pingcap_sso/social_auth_confirm_binding.html' form_class = SocialAuthConfirmBindingExistsAccountForm def dispatch(self, request, *args, **kwargs): if not is_social_auth_new_user(request): return HttpResponseRedirect(reverse('index')) try: self.bind_phone = request.session[SOCIAL_AUTH_BIND_PHONE_SESSION_KEY] self.user = User.objects.get(phone=self.bind_phone) except (KeyError, User.DoesNotExist): return HttpResponseRedirect(reverse('social-auth-add-phone')) return super().dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['bind_phone'] = self.bind_phone context['bind_user'] = self.user return context def get_form_kwargs(self): provider, uid, extra_data = get_social_auth_data_session(self.request) return { 'provider': provider, 'uid': uid, 'extra_data': extra_data, 'data': {'phone': self.bind_phone} } def form_valid(self, form): clear_social_auth_new_user_session(self.request) user = form.bind_social_auth() if user.is_active: login(self.request, user, backend='pingcap_sso.backends.SimpleModelBackend') else: return HttpResponse('This account has been disabled, please contact the administrator') return HttpResponseRedirect(sync_cookie_url(self.request.session.pop(WECHAT_REDIRECT_SESSION_KEY, reverse('index'))))