Skip to content

Commit cee33f0

Browse files
authored
feat: Allow custom assertion URIs (#348)
* feat: Allow custom assertion URIs * Update README * Remove CUSTOM_ASSERTION_URI * Lambda to func * URI to URL
1 parent df32f5f commit cee33f0

File tree

3 files changed

+37
-5
lines changed

3 files changed

+37
-5
lines changed

README.md

+5-2
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,8 @@ python setup.py install
187187
'GET_USER_ID_FROM_SAML_RESPONSE': 'path.to.your.get.user.from.saml.hook.method',
188188
# This can override the METADATA_AUTO_CONF_URL to enumerate all existing metadata autoconf URLs
189189
'GET_METADATA_AUTO_CONF_URLS': 'path.to.your.get.metadata.conf.hook.method',
190+
# This will override ASSERTION_URL to allow more dynamic assertion URLs
191+
'GET_CUSTOM_ASSERTION_URL': 'path.to.your.get.custom.assertion.url.hook.method',
190192
},
191193
'ASSERTION_URL': 'https://mysite.com', # Custom URL to validate incoming SAML requests against
192194
'ENTITY_ID': 'https://mysite.com/sso/acs/', # Populates the Issuer element in authn request
@@ -232,7 +234,7 @@ Some of the following settings are related to how this module operates. The rest
232234
<summary>Click to see the module settings</summary>
233235

234236
| **Field name** | **Description** | **Data type(s)** | **Default value(s)** | **Example** |
235-
|---------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------|------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------|
237+
|---------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ---------------- |------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------|
236238
| **METADATA\_AUTO\_CONF\_URL** | Auto SAML2 metadata configuration URL | `str` | `None` | `https://ORG.okta.com/app/APP-ID/sso/saml/metadata` |
237239
| **METADATA\_LOCAL\_FILE\_PATH** | SAML2 metadata configuration file path | `str` | `None` | `/path/to/the/metadata.xml` |
238240
| **KEY_FILE** | SAML2 private key file path. Required for AUTHN\_REQUESTS\_SIGNED | `str` | `None` | `/path/to/the/key.pem` |
@@ -257,7 +259,8 @@ Some of the following settings are related to how this module operates. The rest
257259
| **TRIGGER.CUSTOM\_DECODE\_JWT** | A hook function to decode the user JWT. This method will be called instead of the `decode_jwt_token` default function and should return the user_model.USERNAME_FIELD. This method accepts one parameter: `token`. | `str` | `None` | `my_app.models.users.decode_custom_token` |
258260
| **TRIGGER.CUSTOM\_CREATE\_JWT** | A hook function to create a custom JWT for the user. This method will be called instead of the `create_jwt_token` default function and should return the token. This method accepts one parameter: `user`. | `str` | `None` | `my_app.models.users.create_custom_token` |
259261
| **TRIGGER.CUSTOM\_TOKEN\_QUERY** | A hook function to create a custom query params with the JWT for the user. This method will be called after `CUSTOM_CREATE_JWT` to populate a query and attach it to a URL; should return the query params containing the token (e.g., `?token=encoded.jwt.token`). This method accepts one parameter: `token`. | `str` | `None` | `my_app.models.users.get_custom_token_query` |
260-
| **ASSERTION\_URL** | A URL to validate incoming SAML responses against. By default, `django-saml2-auth` will validate the SAML response's Service Provider address against the actual HTTP request's host and scheme. If this value is set, it will validate against `ASSERTION_URL` instead - perfect for when Django is running behind a reverse proxy. | `str` | `https://example.com` | |
262+
| **TRIGGER.GET\_CUSTOM\_ASSERTION\_URL** | A hook function to get the assertion URL dynamically. Useful when you have dynamic routing, multi-tenant setup and etc. Overrides `ASSERTION_URL`. | `str` | `None` | `my_app.utils.get_custom_assertion_url` |
263+
| **ASSERTION\_URL** | A URL to validate incoming SAML responses against. By default, `django-saml2-auth` will validate the SAML response's Service Provider address against the actual HTTP request's host and scheme. If this value is set, it will validate against `ASSERTION_URL` instead - perfect for when Django is running behind a reverse proxy. This will only allow to customize the domain part of the URL, for more customization use `GET_CUSTOM_ASSERTION_URL`. | `str` | `None` | `https://example.com` |
261264
| **ENTITY\_ID** | The optional entity ID string to be passed in the 'Issuer' element of authentication request, if required by the IDP. | `str` | `None` | `https://exmaple.com/sso/acs` |
262265
| **NAME\_ID\_FORMAT** | Set to the string `'None'`, to exclude sending the `'Format'` property of the `'NameIDPolicy'` element in authentication requests. | `str` | `<urn:oasis:names:tc:SAML:2.0:nameid-format:transient>` | |
263266
| **USE\_JWT** | Set this to the boolean `True` if you are using Django with JWT authentication | `bool` | `False` | |

django_saml2_auth/saml.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,11 @@ def get_metadata(
156156
)
157157

158158

159+
def get_custom_acs_url() -> Optional[str]:
160+
get_custom_acs_url_hook = dictor(settings.SAML2_AUTH, "TRIGGER.GET_CUSTOM_ASSERTION_URL")
161+
return run_hook(get_custom_acs_url_hook) if get_custom_acs_url_hook else None
162+
163+
159164
def get_saml_client(
160165
domain: str,
161166
acs: Callable[..., HttpResponse],
@@ -180,9 +185,6 @@ def get_saml_client(
180185
Returns:
181186
Optional[Saml2Client]: A Saml2Client or None
182187
"""
183-
# get_reverse raises an exception if the view is not found, so we can safely ignore type errors
184-
acs_url = domain + get_reverse([acs, "acs", "django_saml2_auth:acs"]) # type: ignore
185-
186188
get_user_id_from_saml_response = dictor(
187189
settings.SAML2_AUTH, "TRIGGER.GET_USER_ID_FROM_SAML_RESPONSE"
188190
)
@@ -204,6 +206,11 @@ def get_saml_client(
204206
},
205207
)
206208

209+
acs_url = get_custom_acs_url()
210+
if not acs_url:
211+
# get_reverse raises an exception if the view is not found, so we can safely ignore type errors
212+
acs_url = domain + get_reverse([acs, "acs", "django_saml2_auth:acs"]) # type: ignore
213+
207214
saml2_auth_settings = settings.SAML2_AUTH
208215

209216
saml_settings: Dict[str, Any] = {

django_saml2_auth/tests/test_saml.py

+22
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
from django.http import HttpRequest
1212
from django.test.client import RequestFactory
1313
from django.urls import NoReverseMatch
14+
from saml2 import BINDING_HTTP_POST
15+
1416
from django_saml2_auth.exceptions import SAMLAuthError
1517
from django_saml2_auth.saml import (
1618
decode_saml_response,
@@ -113,6 +115,13 @@ def get_metadata_auto_conf_urls(
113115
return [{"url": METADATA_URL1}, {"url": METADATA_URL2}]
114116

115117

118+
def get_custom_assertion_url():
119+
return "https://example.com/custom-tenant/acs"
120+
121+
122+
GET_CUSTOM_ASSERTION_URL = "django_saml2_auth.tests.test_saml.get_custom_assertion_url"
123+
124+
116125
def mock_extract_user_identity(
117126
user: Dict[str, Optional[Any]], authn_response: AuthnResponse
118127
) -> Dict[str, Optional[Any]]:
@@ -457,6 +466,19 @@ def test_get_saml_client_success_with_key_and_cert_files(
457466
del settings.SAML2_AUTH[key]
458467

459468

469+
def test_get_saml_client_success_with_custom_assertion_url_hook(settings: SettingsWrapper):
470+
settings.SAML2_AUTH["METADATA_LOCAL_FILE_PATH"] = "django_saml2_auth/tests/metadata.xml"
471+
472+
settings.SAML2_AUTH["TRIGGER"]["GET_CUSTOM_ASSERTION_URL"] = GET_CUSTOM_ASSERTION_URL
473+
474+
result = get_saml_client("example.com", acs, "[email protected]")
475+
assert result is not None
476+
assert "https://example.com/custom-tenant/acs" in result.config.endpoint(
477+
"assertion_consumer_service",
478+
BINDING_HTTP_POST,
479+
"sp",
480+
)
481+
460482
@responses.activate
461483
def test_decode_saml_response_success(
462484
settings: SettingsWrapper,

0 commit comments

Comments
 (0)