Browse Source

Add SSO user sync api (#1282)

* Add SSO api for user sync

* Tweak naming
Rafał Pitoń 5 years ago
parent
commit
1ab4e6e82d

+ 34 - 0
misago/sso/api.py

@@ -0,0 +1,34 @@
+import jwt
+from django.http import Http404, HttpResponseBadRequest, JsonResponse
+from django.views.decorators.csrf import csrf_exempt
+from django.views.decorators.http import require_POST
+
+from .user import get_or_create_user
+from .validators import UserDataValidator
+
+
+@csrf_exempt
+@require_POST
+def sso_sync(request):
+    if not request.settings.enable_sso:
+        raise Http404()
+
+    access_token = request.POST.get("access_token")
+    if not access_token:
+        return HttpResponseBadRequest("Request did not contain the access token")
+
+    try:
+        user_data = jwt.decode(
+            access_token, request.settings.sso_private_key, algorithms=["HS256"]
+        )
+    except jwt.PyJWTError:
+        return HttpResponseBadRequest("Access token is invalid")
+
+    validator = UserDataValidator(user_data)
+    if not validator.is_valid():
+        failed_fields = ", ".join(validator.errors.keys())
+        return HttpResponseBadRequest(f"User data failed to validate: {failed_fields}")
+
+    user = get_or_create_user(request, validator.cleaned_data)
+
+    return JsonResponse({"id": user.id})

+ 1 - 1
misago/sso/client.py

@@ -35,7 +35,7 @@ def create_configured_client(request):
     settings = request.settings
 
     return ClientMisago(
-        settings.sso_server,
+        settings.sso_url,
         settings.sso_public_key,
         settings.sso_private_key,
         request=request,

+ 69 - 0
misago/sso/tests/test_sso_sync_api.py

@@ -0,0 +1,69 @@
+import jwt
+from django.contrib.auth import get_user_model
+from django.urls import reverse
+
+from ...conf.test import override_dynamic_settings
+from .utils import TEST_SSO_SETTINGS
+
+User = get_user_model()
+api_link = reverse("simple-sso-sync")
+
+
+@override_dynamic_settings(enable_sso=False)
+def test_sso_api_returns_404_if_sso_is_disabled(db, client):
+    response = client.post(api_link)
+    assert response.status_code == 404
+
+
+@override_dynamic_settings(**TEST_SSO_SETTINGS)
+def test_sso_api_returns_400_if_api_request_is_missing_access_token(db, client):
+    response = client.post(api_link)
+    assert response.status_code == 400
+
+
+@override_dynamic_settings(**TEST_SSO_SETTINGS)
+def test_sso_api_returns_400_if_access_token_is_invalid(db, client):
+    response = client.post(api_link, {"access_token": "invalid"})
+    assert response.status_code == 400
+
+
+@override_dynamic_settings(**TEST_SSO_SETTINGS)
+def test_sso_api_returns_400_if_user_data_in_token_is_invalid(db, client):
+    token = jwt.encode(
+        {"username": "jkowalski", "email": "jkowalski@example.com"},
+        TEST_SSO_SETTINGS["sso_private_key"],
+        algorithm="HS256",
+    ).decode("ascii")
+
+    response = client.post(api_link, {"access_token": token})
+    assert response.status_code == 400
+
+
+@override_dynamic_settings(**TEST_SSO_SETTINGS)
+def test_sso_api_creates_user_account_if_user_data_is_valid(db, client):
+    token = jwt.encode(
+        {"id": 1, "username": "jkowalski", "email": "jkowalski@example.com"},
+        TEST_SSO_SETTINGS["sso_private_key"],
+        algorithm="HS256",
+    ).decode("ascii")
+
+    response = client.post(api_link, {"access_token": token})
+    assert response.status_code == 200
+
+    user = User.objects.get(sso_id=1)
+    assert user.username == "jkowalski"
+    assert user.email == "jkowalski@example.com"
+
+
+@override_dynamic_settings(**TEST_SSO_SETTINGS)
+def test_sso_api_returns_user_id_if_user_data_is_valid(db, client):
+    token = jwt.encode(
+        {"id": 1, "username": "jkowalski", "email": "jkowalski@example.com"},
+        TEST_SSO_SETTINGS["sso_private_key"],
+        algorithm="HS256",
+    ).decode("ascii")
+
+    response = client.post(api_link, {"access_token": token})
+
+    user = User.objects.get(sso_id=1)
+    assert response.json() == {"id": user.pk}

+ 1 - 7
misago/sso/tests/test_sso_views.py

@@ -10,16 +10,10 @@ from django.test import override_settings, TestCase
 from django.utils.timezone import now
 
 from ...conf.test import override_dynamic_settings
+from .utils import TEST_SSO_SETTINGS
 
 User = get_user_model()
 
-TEST_SSO_SETTINGS = {
-    "enable_sso": True,
-    "sso_private_key": "priv1",
-    "sso_public_key": "fakeSsoPublicKey",
-    "sso_server": "http://example.com/server/",
-}
-
 SSO_USER_ID = 1
 
 

+ 6 - 0
misago/sso/tests/utils.py

@@ -0,0 +1,6 @@
+TEST_SSO_SETTINGS = {
+    "enable_sso": True,
+    "sso_private_key": "priv1",
+    "sso_public_key": "fakeSsoPublicKey",
+    "sso_url": "http://example.com/server/",
+}

+ 2 - 0
misago/sso/urls.py

@@ -1,8 +1,10 @@
 from django.conf.urls import url
 
+from .api import sso_sync
 from .client import MisagoAuthenticateView, MisagoLoginView
 
 urlpatterns = [
+    url(r"^sync/$", sso_sync, name="simple-sso-sync"),
     url(r"^client/$", MisagoLoginView.as_view(), name="simple-sso-login"),
     url(
         r"^client/authenticate/$",

+ 1 - 0
requirements.in

@@ -12,6 +12,7 @@ Faker<1.1
 html5lib<1.1
 markdown<3
 social-auth-app-django
+pyjwt
 pillow<7
 psycopg2-binary<2.9
 pytest

+ 1 - 1
requirements.txt

@@ -37,7 +37,7 @@ pillow==6.1.0
 pluggy==0.12.0            # via pytest
 psycopg2-binary==2.8.3
 py==1.8.0                 # via pytest
-pyjwt==1.7.1              # via social-auth-core
+pyjwt==1.7.1
 pyparsing==2.4.0          # via packaging
 pytest-django==3.5.1
 pytest-mock==1.10.4