Skip to content

Commit 18e1159

Browse files
committed
feat: idp hinting
- IdP hinting in HTTP-REDIRECT and HTTP-POST - README, added idp hinting section - small code refactor
1 parent 7331ae9 commit 18e1159

File tree

4 files changed

+84
-4
lines changed

4 files changed

+84
-4
lines changed

README.rst

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,13 +164,36 @@ Idp's like Okta require a signed logout response to validate and logout a user.
164164

165165
Discovery Service
166166
-----------------
167-
If you want to use a SAML Discovery Service, all you need is adding:
167+
If you want to use a SAML Discovery Service, all you need is adding::
168168

169169
SAML2_DISCO_URL = 'https://your.ds.example.net/'
170170

171171
Of course, with the real URL of your preferred Discovery Service.
172172

173173

174+
Idp hinting
175+
-----------
176+
If the SP uses an AIM Proxy it is possible to suggest the authentication IDP by adopting the _idphint_ parameter. The name of the `idphint` parameter is default, but it can also be changed using this parameter::
177+
178+
SAML2_IDPHINT_PARAM = 'idphint'
179+
180+
This will ensure that the user will not get a possible discovery service page for the selection of the IdP to use for the SSO.
181+
When Djagosaml2 receives an HTTP request at the resource, web path, configured for the saml2 login, it will detect the presence of the `idphint` parameter. If this is present, the authentication request will report this URL parameter within the http request relating to the SAML2 SSO binding.
182+
183+
For example::
184+
185+
import requests
186+
import urllib
187+
idphint = {'idphint': [
188+
urllib.parse.quote_plus(b'https://that.idp.example.org/metadata'),
189+
urllib.parse.quote_plus(b'https://another.entitydi.org')]
190+
}
191+
param = urllib.parse.urlencode(idphint)
192+
# param is "idphint=%5B%27https%253A%252F%252Fthat.idp.example.org%252Fmetadata%27%2C+%27https%253A%252F%252Fanother.entitydi.org%27%5D"
193+
requests.get(f'http://djangosaml2.sp.fqdn.org/saml2/login/?{param}')
194+
195+
see AARC Blueprint specs `here <https://zenodo.org/record/4596667/files/AARC-G061-A_specification_for_IdP_hinting.pdf>`_.
196+
174197
Changes in the urls.py file
175198
---------------------------
176199

djangosaml2/utils.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,23 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
import base64
15+
import logging
1516
import re
1617
import urllib
1718
import zlib
1819
from typing import Optional
1920

2021
from django.conf import settings
2122
from django.core.exceptions import ImproperlyConfigured
23+
from django.http import HttpResponseRedirect
2224
from django.utils.http import is_safe_url
2325
from saml2.config import SPConfig
2426
from saml2.s_utils import UnknownSystemEntity
2527

2628

29+
logger = logging.getLogger(__name__)
30+
31+
2732
def get_custom_setting(name: str, default=None):
2833
return getattr(settings, name, default)
2934

@@ -106,3 +111,52 @@ def get_session_id_from_saml2(saml2_xml):
106111
def get_subject_id_from_saml2(saml2_xml):
107112
saml2_xml = saml2_xml if isinstance(saml2_xml, str) else saml2_xml.decode()
108113
re.findall('">([a-z0-9]+)</saml:NameID>', saml2_xml)[0]
114+
115+
def add_param_in_url(url:str, param_key:str, param_value:str):
116+
params = list(url.split('?'))
117+
params.append(f'{param_key}={param_value}')
118+
new_url = params[0] + '?' +''.join(params[1:])
119+
return new_url
120+
121+
def add_idp_hinting(request, http_response) -> bool:
122+
idphin_param = getattr(settings, 'SAML2_IDPHINT_PARAM', 'idphint')
123+
params = urllib.parse.urlencode(request.GET)
124+
125+
if idphin_param not in request.GET.keys():
126+
return False
127+
128+
idphint = request.GET[idphin_param]
129+
# validation : TODO -> improve!
130+
if idphint[0:4] != 'http':
131+
logger.warning(
132+
f'Idp hinting: "{idphint}" doesn\'t contain a valid value.'
133+
'idphint paramenter ignored.'
134+
)
135+
return False
136+
137+
if http_response.status_code in (302, 303):
138+
# redirect binding
139+
# urlp = urllib.parse.urlparse(http_response.url)
140+
new_url = add_param_in_url(http_response.url,
141+
idphin_param, idphint)
142+
return HttpResponseRedirect(new_url)
143+
144+
elif http_response.status_code == 200:
145+
# post binding
146+
res = re.search(r'action="(?P<url>[a-z0-9\:\/\_\-\.]*)"',
147+
http_response.content.decode(), re.I)
148+
if not res:
149+
return False
150+
orig_url = res.groupdict()['url']
151+
#
152+
new_url = add_param_in_url(orig_url, idphin_param, idphint)
153+
content = http_response.content.decode()\
154+
.replace(orig_url, new_url)\
155+
.encode()
156+
return HttpResponse(content)
157+
158+
else:
159+
logger.warning(
160+
f'Idp hinting: cannot detect request type [{http_response.status_code}]'
161+
)
162+
return False

djangosaml2/views.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
from .conf import get_config
5353
from .exceptions import IdPConfigurationMissing
5454
from .overrides import Saml2Client
55-
from .utils import (available_idps, get_custom_setting,
55+
from .utils import (add_idp_hinting, available_idps, get_custom_setting,
5656
get_idp_sso_supported_bindings, get_location,
5757
validate_referral_url)
5858

@@ -307,7 +307,10 @@ def get(self, request, *args, **kwargs):
307307
f'Saving the session_id "{oq_cache.__dict__}" '
308308
'in the OutstandingQueries cache',
309309
)
310-
return http_response
310+
311+
# idp hinting support, add idphint url parameter if present in this request
312+
response = add_idp_hinting(request, http_response) or http_response
313+
return response
311314

312315

313316
@method_decorator(csrf_exempt, name='dispatch')

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def read(*rnames):
2424

2525
setup(
2626
name='djangosaml2',
27-
version='1.0.7',
27+
version='1.1.0',
2828
description='pysaml2 integration for Django',
2929
long_description=read('README.rst'),
3030
classifiers=[

0 commit comments

Comments
 (0)