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