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

feat: otp #8

Merged
merged 59 commits into from
Oct 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
d4f5b1a
initial
SKairinos Sep 11, 2023
1ba8153
initial code
SKairinos Sep 12, 2023
194d50d
quick save
SKairinos Sep 12, 2023
cf5526d
install portal instead
SKairinos Sep 12, 2023
a22d25f
auth backends
SKairinos Sep 13, 2023
3598cae
tidy up auth backends
SKairinos Sep 13, 2023
5f1c537
update auth backends
SKairinos Sep 13, 2023
cc510c8
quick save
SKairinos Sep 13, 2023
a78f098
deploy to gcloud
SKairinos Sep 13, 2023
1b1945b
fix pipeline
SKairinos Sep 13, 2023
ae05f02
don't check migrations
SKairinos Sep 13, 2023
c44c8d0
use correct service name
SKairinos Sep 13, 2023
dcea83a
no pytest
SKairinos Sep 13, 2023
b80dccf
use latest cfl packages
SKairinos Sep 13, 2023
d6230b4
quick save
SKairinos Sep 14, 2023
f5fb6eb
include a base url for service routing
SKairinos Sep 14, 2023
b46d789
set base route
SKairinos Sep 14, 2023
22de360
use cfl package
SKairinos Sep 14, 2023
5bd781a
tests and remove user import
SKairinos Sep 14, 2023
393eb70
session config
SKairinos Sep 14, 2023
14a30e2
quick save
SKairinos Sep 15, 2023
681750f
remove logout endpoint
SKairinos Sep 15, 2023
6ed3e50
login working
SKairinos Sep 15, 2023
3201d5f
set session cookie domain
SKairinos Sep 15, 2023
0d5029f
return invalid form errors
SKairinos Sep 15, 2023
d740dbd
login middleware
SKairinos Sep 15, 2023
51d62d7
simplify code
SKairinos Sep 15, 2023
e688085
remove login middleware
SKairinos Sep 15, 2023
4210534
remove extra white spacing
SKairinos Sep 15, 2023
56ec08a
update launch
SKairinos Sep 18, 2023
d8a6067
use new cfl package
SKairinos Sep 18, 2023
78f5ef0
fix pipeline
SKairinos Sep 18, 2023
fe4edb2
raise validation errors
SKairinos Sep 18, 2023
8e63cef
remove todos
SKairinos Sep 18, 2023
2c3140b
use latest package version
SKairinos Sep 19, 2023
a910e05
use latest cfl package
SKairinos Sep 19, 2023
a86a15b
set secret key
SKairinos Sep 19, 2023
429bd25
new cfl package
SKairinos Sep 19, 2023
c0a223f
fix: set env vars
SKairinos Sep 19, 2023
f2a1a68
use new cfl package
SKairinos Sep 19, 2023
7439dec
user new cfl-common package
SKairinos Sep 19, 2023
006c487
house keeping [skip ci]
SKairinos Sep 19, 2023
ba679e7
use latest cfl package
SKairinos Sep 19, 2023
792fa71
feedback
SKairinos Sep 21, 2023
9131425
remove unnecessary return types
SKairinos Sep 21, 2023
8140725
return remaining session auth factors
SKairinos Sep 27, 2023
c1d3081
codeforlife.user
SKairinos Sep 27, 2023
abf35f2
save session data before response
SKairinos Sep 27, 2023
5e30b73
return auth factors
SKairinos Sep 28, 2023
ced3759
update readme
SKairinos Sep 28, 2023
ca32b48
merge from development
SKairinos Sep 28, 2023
db1bbf4
update lock
SKairinos Sep 28, 2023
a539aa4
support backup token authentication
SKairinos Sep 29, 2023
db83876
test otp
SKairinos Sep 29, 2023
c97dd4f
remove users expired sessions
SKairinos Sep 29, 2023
425410f
update lock
SKairinos Sep 29, 2023
9fba8a8
fix mock
SKairinos Sep 29, 2023
ca6f6a2
use new cfl package
SKairinos Oct 2, 2023
c4007b3
remove todo
SKairinos Oct 2, 2023
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# codeforlife-service-template
# codeforlife-sso

[Frontend Docs](docs/frontend)
This repo contains CFL's Single Sign-On (SSO) service. This will be responsible for authenticating users.
6 changes: 3 additions & 3 deletions backend/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ verify_ssl = true
name = "pypi"

[packages]
codeforlife = {ref = "v0.7.14", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"}
codeforlife = {ref = "v0.8.0", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"}
django = "==3.2.20"
djangorestframework = "==3.13.1"
django-cors-headers = "==4.1.0"
# https://pypi.org/user/codeforlife/
cfl-common = "==6.36.2" # TODO: remove
codeforlife-portal = "==6.36.2" # TODO: remove
cfl-common = "==6.37.1" # TODO: remove
codeforlife-portal = "==6.37.1" # TODO: remove
aimmo = "==2.10.6" # TODO: remove
rapid-router = "==5.11.3" # TODO: remove
phonenumbers = "==8.12.12" # TODO: remove
Expand Down
48 changes: 28 additions & 20 deletions backend/Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions backend/api/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ def get_invalid_login_error_message(self):
return "Please enter the correct one-time password."


class OtpBypassTokenAuthForm(BaseAuthForm):
token = forms.CharField(min_length=8, max_length=8)

def get_invalid_login_error_message(self):
return "Must be exactly 8 characters. A token can only be used once."


class EmailAuthForm(BaseAuthForm):
email = forms.EmailField()
password = forms.CharField(strip=False)
Expand Down
55 changes: 49 additions & 6 deletions backend/api/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,55 @@
from unittest.mock import Mock, patch
from unittest.mock import patch

import pyotp
from codeforlife.tests import CronTestCase
from codeforlife.user.models import AuthFactor, User
from django.core import management
from django.test import TestCase
from django.urls import reverse
from django.utils import timezone


class TestClearExpiredView(CronTestCase):
@patch("django.core.management.call_command")
def test_clear_expired_view(self, call_command: Mock):
self.client.get(reverse("clear-expired-sessions"))
class TestLoginView(TestCase):
def setUp(self):
self.user = User.objects.get(id=2)

def test_post__otp(self):
AuthFactor.objects.create(
user=self.user,
type=AuthFactor.Type.OTP,
)

response = self.client.post(
reverse("login", kwargs={"form": "email"}),
data={
"email": self.user.email,
"password": "Password1",
},
)

assert response.status_code == 200
self.assertDictEqual(
response.json(), {"auth_factors": [AuthFactor.Type.OTP]}
)

self.user.userprofile.otp_secret = pyotp.random_base32()
self.user.userprofile.save()

call_command.assert_called_once_with("clearsessions")
totp = pyotp.TOTP(self.user.userprofile.otp_secret)

now = timezone.now()
with patch.object(timezone, "now", return_value=now):
response = self.client.post(
reverse("login", kwargs={"form": "otp"}),
data={"otp": totp.at(now)},
)

assert response.status_code == 200
self.assertDictEqual(response.json(), {"auth_factors": []})


class TestClearExpiredView(CronTestCase):
def test_clear_expired_view(self):
with patch.object(management, "call_command") as call_command:
self.client.get(reverse("clear-expired-sessions"))
call_command.assert_called_once_with("clearsessions")
2 changes: 1 addition & 1 deletion backend/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
include(
[
re_path(
r"^login/(?P<form>email|username|user-id|otp)/$",
r"^login/(?P<form>email|username|user-id|otp|otp-bypass-token)/$",
LoginView.as_view(),
name="login",
),
Expand Down
31 changes: 26 additions & 5 deletions backend/api/views.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import logging

from codeforlife.mixins import CronMixin
from codeforlife.request import HttpRequest
from common.models import UserSession
from django.contrib.auth import login
from django.contrib.auth.views import LoginView as _LoginView
from django.contrib.sessions.models import Session, SessionManager
from django.core.management import call_command
from django.http import HttpResponse, JsonResponse
from django.core import management
from django.http import JsonResponse
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
Expand All @@ -15,13 +16,16 @@
BaseAuthForm,
EmailAuthForm,
OtpAuthForm,
OtpBypassTokenAuthForm,
UserIdAuthForm,
UsernameAuthForm,
)


# TODO: add 2FA logic
class LoginView(_LoginView):
request: HttpRequest

def get_form_class(self):
form = self.kwargs["form"]
if form == "email":
Expand All @@ -30,10 +34,16 @@ def get_form_class(self):
return UsernameAuthForm
elif form == "user-id":
return UserIdAuthForm
elif form == "otp": # TODO: add 2fa logic.
elif form == "otp":
return OtpAuthForm
elif form == "otp-bypass-token":
return OtpBypassTokenAuthForm

def form_valid(self, form: BaseAuthForm):
# Clear expired sessions.
self.request.session.clear_expired(form.user.id)

# Create session (without data).
login(self.request, form.user)

# TODO: use google analytics
Expand All @@ -45,7 +55,18 @@ def form_valid(self, form: BaseAuthForm):
)
UserSession.objects.create(**user_session)

return HttpResponse()
# Save session (with data).
self.request.session.save()

return JsonResponse(
{
"auth_factors": list(
self.request.user.session.session_auth_factors.values_list(
"auth_factor__type", flat=True
)
)
}
)

def form_invalid(self, form: BaseAuthForm):
return JsonResponse(form.errors, status=status.HTTP_400_BAD_REQUEST)
Expand All @@ -61,7 +82,7 @@ def get(self, request):

# Clears expired sessions.
# https://docs.djangoproject.com/en/3.2/ref/django-admin/#clearsessions
call_command("clearsessions")
management.call_command("clearsessions")

after_session_count = session_objects.count()
logging.info(f"Session count after clearance: {after_session_count}")
Expand Down
2 changes: 1 addition & 1 deletion backend/app.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ env_variables:
SECRET_KEY: "REPLACE_ME"
SERVICE_NAME: "REPLACE_ME"
SERVICE_PROTOCOL: "https"
SERVICE_DOMAIN: "codeforlife.education"
SERVICE_DOMAIN: "www.codeforlife.education"
SERVICE_PORT: "443"
2 changes: 1 addition & 1 deletion backend/service/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"codeforlife",
"codeforlife.user",
"api",
"aimmo", # TODO: remove this
"game", # TODO: remove this
Expand Down