diff --git a/README.rst b/README.rst index cb0f5446..fa9b8719 100644 --- a/README.rst +++ b/README.rst @@ -161,8 +161,12 @@ How to use? 'ASSERTION_URL': 'https://mysite.com', # Custom URL to validate incoming SAML requests against 'ENTITY_ID': 'https://mysite.com/saml2_auth/acs/', # Populates the Issuer element in authn request 'NAME_ID_FORMAT': FormatString, # Sets the Format property of authn NameIDPolicy element + 'ACCEPTED_TIME_DIFF': 0 # sets the accepted_time_diff 'USE_JWT': False, # Set this to True if you are running a Single Page Application (SPA) with Django Rest Framework (DRF), and are using JWT authentication to authorize client users 'FRONTEND_URL': 'https://myfrontendclient.com', # Redirect URL for the client if you are using JWT auth with DRF. See explanation below + 'CERT_FILE': '' # Public part of the service private/public key pair. Must be a PEM formatted certificate chain file. + 'KEY_FILE': '' # The name of a PEM formatted file that contains the private key of the service. This is presently used both to encrypt/sign assertions and as the client key in an HTTPS session. + 'AUTHN_REQUESTS_SIGNED': False # Indicates if the Authentication Requests sent by this SP should be signed by default. } #. In your SAML2 SSO identity provider, set the Single-sign-on URL and Audience @@ -207,6 +211,8 @@ behind a reverse proxy. **NAME_ID_FORMAT** Set to the string 'None', to exclude sending the 'Format' property of the 'NameIDPolicy' element in authn requests. Default value if not specified is 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'. +**ACCEPTED_TIME_DIFF** Sets the accepted time diff in seconds `PySaml2 Accepted Time Diff `_ + **USE_JWT** Set this to the boolean True if you are using Django Rest Framework with JWT authentication **FRONTEND_URL** If USE_JWT is True, you should set the URL of where your frontend is located (will default to DEFAULT_NEXT_URL if you fail to do so). Once the client is authenticated through the SAML/SSO, your client is redirected to the FRONTEND_URL with the user id (uid) and JWT token (token) as query parameters. diff --git a/django_saml2_auth/views.py b/django_saml2_auth/views.py index 865c112b..6ecb8f78 100644 --- a/django_saml2_auth/views.py +++ b/django_saml2_auth/views.py @@ -19,7 +19,7 @@ from django.shortcuts import render from django.views.decorators.csrf import csrf_exempt from django.template import TemplateDoesNotExist -from django.http import HttpResponseRedirect +from django.http import HttpResponse, HttpResponseRedirect from django.utils.http import is_safe_url from rest_auth.utils import jwt_encode @@ -93,6 +93,8 @@ def _get_saml_client(domain): acs_url = domain + get_reverse([acs, 'acs', 'django_saml2_auth:acs']) metadata = _get_metadata() + authn_requests_signed = settings.SAML2_AUTH.get('AUTHN_REQUESTS_SIGNED', False) + saml_settings = { 'metadata': metadata, 'service': { @@ -104,7 +106,7 @@ def _get_saml_client(domain): ], }, 'allow_unsolicited': True, - 'authn_requests_signed': False, + 'authn_requests_signed': authn_requests_signed, 'logout_requests_signed': True, 'want_assertions_signed': True, 'want_response_signed': False, @@ -118,6 +120,15 @@ def _get_saml_client(domain): if 'NAME_ID_FORMAT' in settings.SAML2_AUTH: saml_settings['service']['sp']['name_id_format'] = settings.SAML2_AUTH['NAME_ID_FORMAT'] + if 'ACCEPTED_TIME_DIFF' in settings.SAML2_AUTH: + saml_settings['accepted_time_diff'] = settings.SAML2_AUTH['ACCEPTED_TIME_DIFF'] + + if settings.SAML2_AUTH.get('CERT_FILE'): + saml_settings['cert_file'] = settings.SAML2_AUTH['CERT_FILE'] + + if settings.SAML2_AUTH.get('KEY_FILE'): + saml_settings['key_file'] = settings.SAML2_AUTH['KEY_FILE'] + spConfig = Saml2Config() spConfig.load(saml_settings) spConfig.allow_unknown_attributes = True @@ -155,9 +166,18 @@ def _create_new_user(username, email, firstname, lastname): @csrf_exempt def acs(r): + try: + import urlparse as _urlparse + from urllib import unquote + except: + import urllib.parse as _urlparse + from urllib.parse import unquote + saml_client = _get_saml_client(get_current_domain(r)) resp = r.POST.get('SAMLResponse', None) next_url = r.session.get('login_next_url', _default_next_url()) + # Use RelayState if available, else fall back to next_url. + next_url = r.POST.get('RelayState', next_url) if not resp: return HttpResponseRedirect(get_reverse([denied, 'denied', 'django_saml2_auth:denied'])) @@ -204,12 +224,22 @@ def acs(r): if settings.SAML2_AUTH.get('USE_JWT') is True: # We use JWT auth send token to frontend jwt_token = jwt_encode(target_user) - query = '?uid={}&token={}'.format(target_user.id, jwt_token) + params = {"uid": target_user.id, "token": jwt_token} frontend_url = settings.SAML2_AUTH.get( 'FRONTEND_URL', next_url) - return HttpResponseRedirect(frontend_url+query) + if next_url and next_url != _default_next_url(): + frontend_url = next_url + + # Reconstruct URL with added parameters. + url_parts = list(_urlparse.urlparse(frontend_url, allow_fragments=False)) + query = dict(_urlparse.parse_qsl(url_parts[4])) + query.update(params) + + url_parts[4] = _urlparse.urlencode(query) + + return HttpResponseRedirect(_urlparse.urlunparse(url_parts)) if is_new_user: try: @@ -247,16 +277,21 @@ def signin(r): r.session['login_next_url'] = next_url saml_client = _get_saml_client(get_current_domain(r)) - _, info = saml_client.prepare_for_authenticate() + _, info = saml_client.prepare_for_authenticate(binding=BINDING_HTTP_POST, relay_state=next_url) + + if info["method"] == "GET": + redirect_url = None - redirect_url = None + for key, value in info['headers']: + if key == 'Location': + redirect_url = value + break - for key, value in info['headers']: - if key == 'Location': - redirect_url = value - break + return HttpResponseRedirect(redirect_url) - return HttpResponseRedirect(redirect_url) + elif info["method"] == "POST": + response_content = info["data"] + return HttpResponse(response_content) def signout(r): diff --git a/setup.py b/setup.py index 2eb6761e..f09920bc 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ setup( name='django_saml2_auth', - version='2.2.1', + version='2.3.0', description='Django SAML2 Authentication Made Easy. Easily integrate with SAML2 SSO identity providers like Okta', long_description=long_description,