Skip to content

Commit

Permalink
passwordless login system with jwt
Browse files Browse the repository at this point in the history
  • Loading branch information
xyb committed Jan 2, 2023
1 parent 05a8ffb commit 2e5b18e
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 7 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,5 @@ dmypy.json

# Pyre type checker
.pyre/

*.sqlite3
71 changes: 65 additions & 6 deletions drf_passwordless_jwt/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
https://docs.djangoproject.com/en/4.1/ref/settings/
"""

from os import getenv
from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
Expand All @@ -20,12 +21,18 @@
# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-yhe(15ptdmm(cbczejzf4x-h)f!=b$vyfw@68+t2y6i_ic!(w@'
SECRET_KEY = getenv(
'DJANGO_SECRET_KEY',
'django-insecure-yhe(15ptdmm(cbczejzf4x-h)f!=b$vyfw@68+t2y6i_ic!(w@',
)

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
DEBUG = bool(int(getenv('DJANGO_DEBUG', 1)))

ALLOWED_HOSTS = []
if getenv('DJANGO_ALLOWED_HOSTS'):
ALLOWED_HOSTS = getenv('DJANGO_ALLOWED_HOSTS').split(',')
else:
ALLOWED_HOSTS = ['*']


# Application definition
Expand All @@ -37,8 +44,49 @@
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'rest_framework.authtoken',
'drfpasswordless',
]

REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
],
}

PASSWORDLESS_AUTH = {
'PASSWORDLESS_AUTH_TYPES': ['EMAIL'],
'PASSWORDLESS_EMAIL_NOREPLY_ADDRESS': getenv('OTP_EMAIL_ADDRESS', '[email protected]'),
'PASSWORDLESS_TOKEN_EXPIRE_TIME': int(getenv('OTP_TOKEN_EXPIRE_SECONDS', 5 * 60)),
}
if getenv('OTP_EMAIL_SUBJECT'):
PASSWORDLESS_AUTH['PASSWORDLESS_EMAIL_SUBJECT'] = getenv('OTP_EMAIL_SUBJECT')
if getenv('OTP_EMAIL_PLAINTEXT'):
PASSWORDLESS_AUTH['PASSWORDLESS_EMAIL_PLAINTEXT_MESSAGE'] = getenv('OTP_EMAIL_PLAINTEXT')
if getenv('OTP_EMAIL_HTML'):
PASSWORDLESS_AUTH['PASSWORDLESS_EMAIL_TOKEN_HTML_TEMPLATE_NAME'] = getenv('OTP_EMAIL_HTML')

if getenv('EMAIL_BACKEND_TEST'):
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
else:
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_USE_SSL = bool(int(getenv('EMAIL_USE_SSL', 0)))
EMAIL_USE_TLS = bool(int(getenv('EMAIL_USE_TLS', 0)))
EMAIL_HOST = getenv('EMAIL_HOST', 'smtp.mydomain.com')
EMAIL_HOST_USER = getenv('EMAIL_HOST_USER', '[email protected]')
EMAIL_HOST_PASSWORD = getenv('EMAIL_HOST_PASSWORD', 'password')
EMAIL_PORT = int(getenv('EMAIL_PORT', 465))
EMAIL_FROM = getenv('EMAIL_FROM', '[email protected]')
EMAIL_TIMEOUT = int(getenv('EMAIL_TIMEOUT', 3))
EMAIL_WHITE_LIST = getenv('EMAIL_WHITE_LIST', r'.*')
EMAIL_WHITE_LIST_MESSAGE = getenv('EMAIL_WHITE_LIST_MESSAGE',
'email address not in white list')

JWT_SECRET = getenv('JWT_SECRET', 'your secret key')
JWT_EXPIRE_SECONDS = int(getenv('JWT_EXPIRE_SECONDS', 60 * 60 * 24 * 30))


MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
Expand All @@ -54,7 +102,7 @@
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
Expand All @@ -75,10 +123,21 @@

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
'ENGINE': getenv('DB_ENGINE', 'django.db.backends.sqlite3'),
'NAME': getenv('DB_NAME', BASE_DIR / 'db.sqlite3'),
'USER': getenv('DB_USER', 'postgres'),
'PASSWORD': getenv('DB_PASSWORD', ''),
'HOST': getenv('DB_HOST', ''),
'PORT': getenv('DB_PORT', ''),
}
}
if 'mysql' in DATABASES['default']['ENGINE']:
DATABASES['default']['OPTIONS'] = {
# fix mysql error 1452
"init_command": "SET foreign_key_checks = 0;",
# fix mysql emoji issue
'charset': 'utf8mb4',
}


# Password validation
Expand Down
11 changes: 10 additions & 1 deletion drf_passwordless_jwt/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,17 @@
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
from django.urls import include, path
from drfpasswordless.settings import api_settings

from .views import ObtainEmailWhiteListCallbackToken, ObtainJWTFromCallbackToken, VerifyJWT

urlpatterns = [
path(api_settings.PASSWORDLESS_AUTH_PREFIX,
VerifyJWT.as_view(), name='verify_jwt_token'),
path(api_settings.PASSWORDLESS_AUTH_PREFIX + 'jwt/',
ObtainJWTFromCallbackToken.as_view(), name='auth_jwt_token'),
path(api_settings.PASSWORDLESS_AUTH_PREFIX + 'email/',
ObtainEmailWhiteListCallbackToken.as_view(), name='auth_email_token'),
path('admin/', admin.site.urls),
]
19 changes: 19 additions & 0 deletions drf_passwordless_jwt/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from datetime import datetime, timedelta, timezone

import jwt
from django.conf import settings


def generate_jwt(email):
payload = {"email": email}
exp = timedelta(seconds=settings.JWT_EXPIRE_SECONDS)
payload['exp'] = datetime.now(tz=timezone.utc) + exp
token = jwt.encode(payload, settings.JWT_SECRET, algorithm="HS256")
return token


def decode_jwt(token):
payload = jwt.decode(token, settings.JWT_SECRET, algorithms=["HS256"])
ts = payload['exp']
payload['exp'] = datetime.fromtimestamp(ts, timezone.utc)
return payload
63 changes: 63 additions & 0 deletions drf_passwordless_jwt/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import jwt
from drfpasswordless.views import ObtainAuthTokenFromCallbackToken
from rest_framework import serializers, status
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView

from .utils import decode_jwt, generate_jwt

from drfpasswordless.views import ObtainEmailCallbackToken
from drfpasswordless.serializers import EmailAuthSerializer
from django.conf import settings
from django.core.validators import RegexValidator


class EmailAuthWhiteListSerializer(EmailAuthSerializer):
email_regex = RegexValidator(
regex=settings.EMAIL_WHITE_LIST,
message=settings.EMAIL_WHITE_LIST_MESSAGE,
)
email = serializers.EmailField(validators=[email_regex])


class ObtainEmailWhiteListCallbackToken(ObtainEmailCallbackToken):
serializer_class = EmailAuthWhiteListSerializer


class ObtainJWTFromCallbackToken(ObtainAuthTokenFromCallbackToken):
def post(self, request, *args, **kwargs):
email = request.data['email']
resp = super(ObtainJWTFromCallbackToken, self).post(request, *args,
**kwargs)
token = generate_jwt(email)
resp.data['email'] = email
resp.data['token'] = token
return resp


class JWTSerializer(serializers.Serializer):
token = serializers.CharField()

def validate_token(self, value):
try:
value = decode_jwt(value)
except jwt.ExpiredSignatureError:
raise serializers.ValidationError('token expired')
return value


class VerifyJWT(APIView):
permission_classes = [AllowAny]
serializer_class = JWTSerializer

def post(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data,
context={'request': request})
if serializer.is_valid(raise_exception=False):
return Response(
serializer.validated_data['token'],
status=status.HTTP_200_OK,
)

return Response(status=status.HTTP_401_UNAUTHORIZED)
4 changes: 4 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Django
djangorestframework
drfpasswordless-gstr169
PyJWT
10 changes: 10 additions & 0 deletions templates/passwordless_zh_token_email.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>你的登录验证码</title>
</head>
<body>
<h2>你的登录验证码是 {{ callback_token }}。本条验证码有效期5分钟。</h2>
</body>
</html>

0 comments on commit 2e5b18e

Please sign in to comment.