Groups keyboard shortcuts have been updated
Dismiss
See shortcuts

converting from LDAP to SAML2 authentication

34 views
Skip to first unread message

Frederick Yankowski

unread,
Dec 4, 2024, 11:14:40 AM12/4/24
to web2py-users
I have a legacy web2py app that's been using LDAP since 2010 when I created it. We are changing our IT systems to use SAML2 via OneLogin.com for authentication where possible and so I'd like to convert my app from from LDAP to SAML2 authentication. Can anyone provide some examples of SAML2 auth in web2py? 

I've found a few examples but they are old and don't quite give me enough basis to get SAML2 working. I don't want to go through any intermediary like Jainrain. I've looked over gluon/contrib/login_methods/saml2_auth.py but haven't been able to work out all that I need to do to use it.

I'm also concerned about how converting to SAML would work with all the auth_user records already created via the LDAP authentication.

-Fred

simon...@gmail.com

unread,
Dec 5, 2024, 9:57:26 AM12/5/24
to web2py-users
Hello Fred,

I had a legacy web2py app until recently that worked with SAML2 authentication and Azure AD as Identity Provider. In my particular case, all users already existed in my app, only authentication was handled via SAML2.
The following example code was working in my app.
I also recommend to enable python logging and use a browser plugin to inspect saml2 requests for debugging.

1) setup saml2 as authentication method somewhere in the model (db.py in my case). I also disabled some of the other actions.

# ---------------------------------------------------------------------------
# Enable SAML login
# ---------------------------------------------------------------------------
from gluon.contrib.login_methods.saml2_auth import Saml2Auth
import os

# logging:
# https://docs.python.org/3/library/logging.html#levels
import logging
logger = logging.getLogger('saml2')
logger.setLevel(logging.DEBUG)

# Session Cookies is not passed in POST to SP after authenticatoin if samesite is not "none" (must be combined with "secure")
session.samesite('none')
session.secure()

if not request.is_local:
    auth.settings.actions_disabled.append('change_password')
    auth.settings.actions_disabled.append('request_reset_password')
    auth.settings.actions_disabled.append('reset_password')
    auth.settings.actions_disabled.append('profile')
    auth.settings.actions_disabled.append('logout')
    auth.settings.actions_disabled.append('retrieve_username')
    auth.settings.cas_create_user = False
    auth.settings.use_username = True
    auth.settings.logout_next = ''
    auth.settings.login_form = Saml2Auth(
    config_file=os.path.join(request.folder, 'private', 'sp_conf'),
    maps=dict(
        username=lambda v: v['NameID'][0],
        first_name=lambda v: v['First Name'][0],
        last_name=lambda v: v['Last Name'][0],
    ),
    entityid='[entidyId of your security token service provider]',

)


2)  put the make_metadata.py in your private-folder

3) add a controller to view / create the metadata, in my case in default.py:

def metadata():
    import os.path
    if os.path.exists(request.folder + '/private/sp.xml'):
        f = open(request.folder + '/private/sp.xml', 'r')
        response.headers['Content-Type']='application/xml'
        return f.read()
    else:
        import subprocess
        command = 'make_metadata.py ' +  request.folder + '/private/sp_conf.py > ' + request.folder + '/private/sp.xml'
        #In production should be shell=False
        p=subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,shell=True)
        stdout,stderr = p.communicate()
        status = p.poll()
        if status == 0:
            f = open(request.folder + '/private/sp.xml', 'r')
            response.headers['Content-Type']='application/xml'
            return f.read()
        return str(stderr)

4) add sp_conf.py to "private" folder for configuring the metadata. add your specific hostnames, appname, certificate and path to federation metadata (I had that stored locally):

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from saml2 import BINDING_HTTP_POST, BINDING_HTTP_REDIRECT
import os.path
import requests
import tempfile

BASEDIR = os.path.abspath(os.path.dirname(__file__))


def full_path(local_file):
    return os.path.join(BASEDIR, local_file)


# Web2py SP url and application name
HOST = '[FQDN of your host]'
APP = '[web2py application name]'
# To load the IDP metadata...
IDP_METADATA = full_path('FederationMetadata.xml')  # the name of the FederationMEtadata of your STS

CONFIG = {
    # your entity id, usually your subdomain plus the url to the metadata view.
    'entityid': '%s/%s/default/metadata' % (HOST, APP),
    'service': {
        'sp': {
            'name': '[name of your app]',
            'endpoints': {
                'assertion_consumer_service': [
                    ('%s/%s/default/user/login' % (HOST, APP), BINDING_HTTP_REDIRECT),
                    ('%s/%s/default/user/login' % (HOST, APP), BINDING_HTTP_POST),
                ],
            },
            'signing_algorithm': 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
            'authn_requests_signed': True,
            'want_response_signed': False,
            'want_assertions_signed': True,
            'allow_unsolicited': False,
        },
    },
   
    # # Your private and public key, here in a subfolder of "private"
    'key_file': full_path('pki/privateKey.key'),
    'cert_file': full_path('pki/certificate.crt'),

    'attribute_map_dir': full_path('attribute_map_dir'),

    # where the remote metadata is stored
    'metadata': {
        'local': [IDP_METADATA],
    },
}

5) add an "attribute_map_dir" folder with a "saml_unspecified.py" that mapps the field names of your app to the field names of your identity provider, e.g:
MAP = {
    "identifier": "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified",
    "fro": {
        'Last Name': 'last_name',
        'First Name': 'first_name',
        'NameID': 'username',
        'Email': 'email',
        },
    "to": {
        'last_name': 'Last Name',
        'first_name': 'First Name',
        'username': 'NameID',
        'email': 'Email'
    }
}

6) call the metadata-controller to create the sp.xml

That should do it.

Simon

Frederick Yankowski

unread,
Dec 5, 2024, 10:47:45 AM12/5/24
to web2py-users
Thank you, Simon. I had started down a similar path and got stuck. I will use your code code as a basis to continue that work.
Reply all
Reply to author
Forward
0 new messages