Skip to content

Commit 906ac67

Browse files
authored
When USE_JWT is set, log users in to the django admin console as well return the JWT (#361)
* Move use_jwt block below login block * Update test to verify the user is logged in to both the Single Page App and the Django admin console * Fix linting
1 parent 801bdb2 commit 906ac67

File tree

2 files changed

+94
-19
lines changed

2 files changed

+94
-19
lines changed

django_saml2_auth/tests/test_saml.py

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@
99
from django.contrib.sessions.middleware import SessionMiddleware
1010
from unittest.mock import MagicMock
1111
from django.http import HttpRequest
12-
from django.test.client import RequestFactory
12+
from django.test.client import RequestFactory, Client
1313
from django.urls import NoReverseMatch
1414
from saml2 import BINDING_HTTP_POST
1515

16+
from django_saml2_auth.errors import INACTIVE_USER
1617
from django_saml2_auth.exceptions import SAMLAuthError
1718
from django_saml2_auth.saml import (
1819
decode_saml_response,
@@ -771,3 +772,76 @@ def test_get_metadata_success_with_custom_trigger(settings: SettingsWrapper):
771772
get_metadata(domain="not-mapped-example.com")
772773

773774
assert str(exc_info.value) == "Domain not-mapped-example.com not mapped!"
775+
776+
777+
@pytest.mark.django_db
778+
@responses.activate
779+
def test_acs_view_with_use_jwt_both_redirects_user_and_sets_cookies(
780+
settings: SettingsWrapper,
781+
monkeypatch: "MonkeyPatch", # type: ignore # noqa: F821
782+
):
783+
"""Test Acs view when USE_JWT is set, the user is redirected and cookies are set"""
784+
responses.add(responses.GET, METADATA_URL1, body=METADATA1)
785+
settings.SAML2_AUTH = {
786+
"DEFAULT_NEXT_URL": "default_next_url",
787+
"USE_JWT": True,
788+
"JWT_SECRET": "JWT_SECRET",
789+
"JWT_ALGORITHM": "HS256",
790+
"FRONTEND_URL": "https://app.example.com/account/login/saml",
791+
"TRIGGER": {
792+
"BEFORE_LOGIN": None,
793+
"AFTER_LOGIN": None,
794+
"GET_METADATA_AUTO_CONF_URLS": GET_METADATA_AUTO_CONF_URLS,
795+
},
796+
}
797+
monkeypatch.setattr(
798+
Saml2Client, "parse_authn_request_response", mock_parse_authn_request_response
799+
)
800+
client = Client()
801+
response = client.post("/acs/", {"SAMLResponse": "SAML RESPONSE", "RelayState": "/"})
802+
803+
# Response includes a redirect to the single page app, with the JWT in the query string.
804+
assert response.status_code == 302
805+
assert "https://app.example.com/account/login/saml?token=eyJ" in getattr(response, "url")
806+
# Response includes a session id cookie (i.e. the user is logged in to the django admin console)
807+
assert response.cookies.get("sessionid")
808+
809+
810+
@pytest.mark.django_db
811+
@responses.activate
812+
def test_acs_view_use_jwt_set_inactive_user(
813+
settings: SettingsWrapper,
814+
monkeypatch: "MonkeyPatch", # type: ignore # noqa: F821
815+
):
816+
"""Test Acs view when USE_JWT is set that inactive users can not log in"""
817+
responses.add(responses.GET, METADATA_URL1, body=METADATA1)
818+
settings.SAML2_AUTH = {
819+
"DEFAULT_NEXT_URL": "default_next_url",
820+
"USE_JWT": True,
821+
"JWT_SECRET": "JWT_SECRET",
822+
"JWT_ALGORITHM": "HS256",
823+
"FRONTEND_URL": "https://app.example.com/account/login/saml",
824+
"TRIGGER": {
825+
"BEFORE_LOGIN": None,
826+
"AFTER_LOGIN": None,
827+
"GET_METADATA_AUTO_CONF_URLS": GET_METADATA_AUTO_CONF_URLS,
828+
},
829+
}
830+
post_request = RequestFactory().post(METADATA_URL1, {"SAMLResponse": "SAML RESPONSE"})
831+
monkeypatch.setattr(
832+
Saml2Client, "parse_authn_request_response", mock_parse_authn_request_response
833+
)
834+
created, mock_user = user.get_or_create_user(
835+
{"username": "test@example.com", "first_name": "John", "last_name": "Doe"}
836+
)
837+
mock_user.is_active = False
838+
mock_user.save()
839+
monkeypatch.setattr(user, "get_or_create_user", (created, mock_user))
840+
841+
middleware = SessionMiddleware(MagicMock())
842+
middleware.process_request(post_request)
843+
post_request.session.save()
844+
845+
result = acs(post_request)
846+
assert result.status_code == 500
847+
assert f"Error code: {INACTIVE_USER}" in result.content.decode()

django_saml2_auth/views.py

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -149,24 +149,6 @@ def acs(request: HttpRequest):
149149

150150
request.session.flush()
151151

152-
use_jwt = dictor(saml2_auth_settings, "USE_JWT", False)
153-
if use_jwt and target_user.is_active:
154-
# Create a new JWT token for IdP-initiated login (acs)
155-
jwt_token = create_custom_or_default_jwt(target_user)
156-
custom_token_query_trigger = dictor(saml2_auth_settings, "TRIGGER.CUSTOM_TOKEN_QUERY")
157-
if custom_token_query_trigger:
158-
query = run_hook(custom_token_query_trigger, jwt_token)
159-
else:
160-
query = f"?token={jwt_token}"
161-
162-
# Use JWT auth to send token to frontend
163-
frontend_url = dictor(saml2_auth_settings, "FRONTEND_URL", next_url)
164-
custom_frontend_url_trigger = dictor(saml2_auth_settings, "TRIGGER.GET_CUSTOM_FRONTEND_URL")
165-
if custom_frontend_url_trigger:
166-
frontend_url = run_hook(custom_frontend_url_trigger, relay_state) # type: ignore
167-
168-
return HttpResponseRedirect(frontend_url + query)
169-
170152
if target_user.is_active:
171153
# Try to load from the `AUTHENTICATION_BACKENDS` setting in settings.py
172154
if hasattr(settings, "AUTHENTICATION_BACKENDS") and settings.AUTHENTICATION_BACKENDS:
@@ -190,6 +172,25 @@ def acs(request: HttpRequest):
190172
},
191173
)
192174

175+
use_jwt = dictor(saml2_auth_settings, "USE_JWT", False)
176+
if use_jwt:
177+
# Create a new JWT token for IdP-initiated login (acs)
178+
jwt_token = create_custom_or_default_jwt(target_user)
179+
custom_token_query_trigger = dictor(saml2_auth_settings, "TRIGGER.CUSTOM_TOKEN_QUERY")
180+
if custom_token_query_trigger:
181+
query = run_hook(custom_token_query_trigger, jwt_token)
182+
else:
183+
query = f"?token={jwt_token}"
184+
185+
# Use JWT auth to send token to frontend
186+
frontend_url = dictor(saml2_auth_settings, "FRONTEND_URL", next_url)
187+
custom_frontend_url_trigger = dictor(saml2_auth_settings, "TRIGGER.GET_CUSTOM_FRONTEND_URL")
188+
if custom_frontend_url_trigger:
189+
frontend_url = run_hook(custom_frontend_url_trigger, relay_state) # type: ignore
190+
191+
return HttpResponseRedirect(frontend_url + query)
192+
193+
193194
def redirect(redirect_url: Optional[str] = None) -> HttpResponseRedirect:
194195
"""Redirect to the redirect_url or the root page.
195196

0 commit comments

Comments
 (0)