Redirect loop in django-saml2-auth-ai and in djangosaml2

Question:

Good afternoon experts,

I am trying to implement SAML authentication in my web app.
I tried to use django-saml2-auth-ai and in djangosaml2 libraries but I got into a redirect loop for both libraries.

django-saml2-auth-ai (2.1.6)

urls.py:

 url(r'^saml2_auth/', include('django_saml2_auth.urls')),
 url(r'^accounts/login/$', django_saml2_auth.views.signin, name='login'),
 url(r'^accounts/logout/$', django_saml2_auth.views.signout, name='logout'),

config:
I added django_saml2_auth to INSTALLED_APPS.

SAML2_AUTH = {
    'SAML_CLIENT_SETTINGS': {  # Pysaml2 Saml client settings
        'entityid': 'http://localhost:7000/saml2_auth/acs/',
        'metadata': {
            'local': [
                os.path.join(BASE_DIR, 'management_app/azure-ad-metadata.xml'),
            ],
        },
        'service': {
            'sp': {
                'logout_requests_signed': True,
                'idp': 'https://sts.windows.net/8d469bba-ae86-4fe1-a36d-fa9d26ec8ab6/'
            }
        }
    },
    'debug': 1,
    'DEFAULT_NEXT_URL': '/dashboard',
    'NEW_USER_PROFILE': {
        'USER_GROUPS': [],  # The default group name when a new user logs in
        'ACTIVE_STATUS': True,  # The default active status for new users
        'STAFF_STATUS': False,  # The staff status for new users
        'SUPERUSER_STATUS': False,  # The superuser status for new users
    },
    'ATTRIBUTES_MAP': {  # Change Email/UserName/FirstName/LastName to corresponding SAML2 userprofile attributes.
        'email': 'name',
        'username': 'name',
        'first_name': 'givenname',
        'last_name': 'surname',
    },
    'ASSERTION_URL': 'http://localhost:7000',
}

Azure Ad’s basic SAML Configuration

Identifier (Entity ID)                     : http://localhost:7000/saml2_auth/acs/
Reply URL (Assertion Consumer Service URL) : http://localhost:7000/saml2_auth/acs/

djangosaml2 (1.4.0)

urls.py:

 url(r'saml2/', include('djangosaml2.urls')),

config:
I added djangosaml2 to INSTALLED_APPS and djangosaml2.middleware.SamlSessionMiddleware to MIDDLEWARE.

SAML_SESSION_COOKIE_NAME = 'saml_session'
SESSION_COOKIE_SECURE = True
SAML_DJANGO_USER_MAIN_ATTRIBUTE = 'email'
SAML_ATTRIBUTE_MAPPING = {
    'uid': ('username', ),
    'mail': ('email', ),
    'cn': ('first_name', ),
    'sn': ('last_name', ),
}
AUTHENTICATION_BACKENDS = (
    'django.contrib.auth.backends.ModelBackend',
    'djangosaml2.backends.Saml2Backend',
)
LOGIN_URL = '/saml2/login/'
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
import saml2
SAML_DEFAULT_BINDING = saml2.BINDING_HTTP_POST
SAML_LOGOUT_REQUEST_PREFERRED_BINDING = saml2.BINDING_HTTP_POST
SAML_IGNORE_LOGOUT_ERRORS = True
SAML_CREATE_UNKNOWN_USER = True
SAML_USE_NAME_ID_AS_USERNAME = True
from os import path
import saml2.saml
SAML_CONFIG = {
  # full path to the xmlsec1 binary program
  'xmlsec_binary': '/usr/bin/xmlsec1',

  # your entity id, usually your subdomain plus the url to the metadata view
  'entityid': 'http://localhost:7000/saml2/acs/',

  # directory with attribute mapping
  'attribute_map_dir': path.join(BASE_DIR, 'management_app/attribute-maps'),

  # Permits to have attributes not configured in attribute-mappings
  # otherwise...without OID will be rejected
  'allow_unknown_attributes': True,

  # this block states what services we provide
  'service': {
      # we are just a lonely SP
      'sp' : {
          'name': 'SP',
          'name_id_format': saml2.saml.NAMEID_FORMAT_TRANSIENT,

          # For Okta add signed logout requests. Enable this:
          # "logout_requests_signed": True,

          'endpoints': {
              # url and binding to the assetion consumer service view
              # do not change the binding or service name
              'assertion_consumer_service': [
                  ('http://localhost:7000/saml2/acs/', saml2.BINDING_HTTP_POST),
                  ('http://localhost:7000/saml2/acs/', saml2.BINDING_HTTP_REDIRECT),
              ],
              # url and binding to the single logout service view
              # do not change the binding or service name
              'single_logout_service': [
                  # Disable next two lines for HTTP_REDIRECT for IDP's that only support HTTP_POST. Ex. Okta:
                  ('http://localhost:7000/saml2/ls/', saml2.BINDING_HTTP_REDIRECT),
                  ('http://localhost:7000/saml2/ls/post', saml2.BINDING_HTTP_POST),
               ],
           },

          'signing_algorithm':  saml2.xmldsig.SIG_RSA_SHA256,
          'digest_algorithm':  saml2.xmldsig.DIGEST_SHA256,

           # Mandates that the identity provider MUST authenticate the
           # presenter directly rather than rely on a previous security context.
          'force_authn': False,

           # Enable AllowCreate in NameIDPolicy.
          'name_id_format_allow_create': False,

           # attributes that this project need to identify a user
          'required_attributes': ['email'],

           # attributes that may be useful to have but not required
          'optional_attributes': ['surname'],

          'want_response_signed': False,
          'authn_requests_signed': False,
          'logout_requests_signed': True,
          # Indicates that Authentication Responses to this SP must
          # be signed. If set to True, the SP will not consume
          # any SAML Responses that are not signed.
          'want_assertions_signed': True,

          'only_use_keys_in_metadata': True,

          # When set to true, the SP will consume unsolicited SAML
          # Responses, i.e. SAML Responses for which it has not sent
          # a respective SAML Authentication Request.
          'allow_unsolicited': True,

          # in this section the list of IdPs we talk to are defined
          # This is not mandatory! All the IdP available in the metadata will be considered instead.
          'idp': {
              # we do not need a WAYF service since there is
              # only an IdP defined here. This IdP should be
              # present in our metadata

              # the keys of this dictionary are entity ids
              'https://localhost/simplesaml/saml2/idp/metadata.php': {
                  'single_sign_on_service': {
                      saml2.BINDING_HTTP_REDIRECT: 'https://localhost/simplesaml/saml2/idp/SSOService.php',
                      },
                  'single_logout_service': {
                      saml2.BINDING_HTTP_REDIRECT: 'https://localhost/simplesaml/saml2/idp/SingleLogoutService.php',
                      },
                  },
              },
          },
      },

  # where the remote metadata is stored, local, remote or mdq server.
  # One metadatastore or many ...
  'metadata': {
      'local': [path.join(BASE_DIR, 'management_app/azure-ad-metadata.xml')],
      'remote': [{"url": "https://login.microsoftonline.com/8d469bba-ae86-4fe1-a36d-fa9d26ec8ab6/federationmetadata/2007-06/federationmetadata.xml?appid=3bf6313c-fee7-4925-8c66-b94d7dc44bb3"},],
      # 'mdq': [{"url": "https://ds.testunical.it",
      #          "cert": "certficates/others/ds.testunical.it.cert",
      #         }]
      },

  # set to 1 to output debugging information
  'debug': 1,

  # Signing
  'key_file': path.join(BASE_DIR, 'management_app/azure_ad_sso_saml_signing_private.key'),  # private part
  'cert_file': path.join(BASE_DIR, 'management_app/azure_ad_sso_saml_signing_public.cert'),  # public part

  # Encryption
  'encryption_keypairs': [{
      'key_file': path.join(BASE_DIR, 'management_app/azure_ad_sso_saml_signing_private.key'),  # private part
      'cert_file': path.join(BASE_DIR, 'management_app/azure_ad_sso_saml_signing_public.cert'),  # public part
  }],

  # own metadata settings
  'contact_person': [
      {'given_name': 'Lorenzo',
       'sur_name': 'Gil',
       'company': 'Yaco Sistemas',
       'email_address': '[email protected]',
       'contact_type': 'technical'},
      {'given_name': 'Angel',
       'sur_name': 'Fernandez',
       'company': 'Yaco Sistemas',
       'email_address': '[email protected]',
       'contact_type': 'administrative'},
      ],
  # you can set multilanguage information here
  'organization': {
      'name': [('Yaco Sistemas', 'es'), ('Yaco Systems', 'en')],
      'display_name': [('Yaco', 'es'), ('Yaco', 'en')],
      'url': [('http://www.yaco.es', 'es'), ('http://www.yaco.com', 'en')],
      },
  }

Azure Ad’s basic SAML Configuration

Identifier (Entity ID)                     : http://localhost:7000/saml2/acs/
Reply URL (Assertion Consumer Service URL) : http://localhost:7000/saml2/acs/

I think I could authenticate against Azure AD with both libraries but when the token is retrieved to django-saml2-auth-ai or djangosaml2, it got into a redirect loop. I checked forums where similar issue occurred but unfortunately their solution didn’t work for me.

Could you please give me any hint what goes wrong?

Thanks!

+++++++++++ UPDATE 1 +++++++++++++++

It seems that the issue was that I use custom permission which Azure AD doesn’t send so Django rejects it and redirects it to log in. However the cookie is there so the same user will be logged in automatically which Django rejects, etc.

Any help please how I could display the Azure AD login page if the user doesn’t have the permission? Thanks!

Asked By: Viktor

||

Answers:

The problem was that I use custom permission but the logged in (in Azure AD) user didn’t have it -> please see the UPDATE 1 section

I changed the code to raise an exception if the user doesn’t have the permission

@permission_required('XXX', raise_exception=True)

So this 403 error can be handled with

handler403 = 'XXX.custom_403'
Answered By: Viktor