Skip to content

Commit

Permalink
ADD support for SAML2 auth
Browse files Browse the repository at this point in the history
This has two components, a regular Webwork Authen module and a
Mojolicious plugin for SAML2.

The plugin implements a SAML2 SP using the Net::SAML2 library.
Net::SAML2 claims to be compatible with many SAML2 implementations,
including Shibboleth.  The intent for this plugin is to replace the old
Shibboleth auth that depended on the shibd service which requires
Apache's mod_shib.

The WeBWorK::Authen::Saml2 authen module's main purpose is to call the
Saml2 plugin helper sendLoginRequest() that properly sends the user to
the IdP to authenticate. There is a bypass option that allows skipping
the Saml2 authen in favour of the authen module after it. So you can,
for example, put Basic_TheLastOption after it allow access to the
internal username/password auth.

It seems to be standard for Mojolicous plugins to be located in
lib/Mojolicious/Plugin, so I've put the Saml2 plugin there. Additional
detail and configuration instructions can be found in
lib/Mojolicious/Plugin/Saml2/README.md

The Saml2 plugin will only be loaded if the corresponding conf file
exists. Copy conf/authen_saml2.dist.yml to conf/authen_saml2.yml to
enable it. 3 settings in the conf are crucial, the idp.metadata_url must
be set to the IdP's xml metadata endpoint and a unique sp.cert &
sp.signing_key pair should be generated. The example cert and
signing_key must not be used for prod under any circumstances.

I initially put the config in conf/mojolicious.webwork.yml under a saml2
section but seeing as how other authen modules have their own conf
files, I figure it was better to follow that convention. And as there
seems to be more work around the yaml config, I've made the saml2 authen
conf also a yaml file.  The NotYAMLConfig plugin for reading yaml conf
files gave me some trouble though, as I didn't want to step on the main
conf and had to read the code to figure out how to use just the load
conf file functionality.

The Saml2 plugin will generate its own xml metadata that can be used by
the IdP for configuration. This is available at the /saml2/metadata URL
under the default config. Note that endpoints are configurable as I
wanted to be able to change them to match shibd endpoints.

The Saml2 plugin has its own set of controllers and a router for them.
The AcsPostController is the most important one as it handles the
SAMLResponse that sends the user back to Webwork from the IdP. The
errors aren't the most friendly unfortunately, should probably add a
proper 401 error template so users don't see the more scary stacktrace
one.

Note that unlike shibd, attribute maps are not supported. So you
probably have to replace user friendly attribute names like
"studentNumber" with URN style names like
"urn:mace:dir:attribute-def:studentNumber". You can check your IdP's
attribute map xml for the official URN names.

Some discussion about alternatives that I tried before settling on the
Mojolicious plugin approach:

The Saml2 as a plugin idea started after trying out
Mojolicious::Plugin::SAML.  Mojolicious::Plugin::SAML didn't work out in
the end due to two downsides. The major one being a lack of RelayState
support. RelayState is the defacto standard way for SPs to store where
the user wants to go after returning from a successful auth from the
IdP. This is a necessary feature for Webwork as auth to each course is
handled separately and we need to know exactly what course the user
wants to get into. The minor issue is that it doesn't parse the
SAMLResponse attributes for you and I really didn't want to have to muck
with xml parsing.

Apache mod_proxy using mod_shib and shibd was another possibility. This
requires passing shib params through HTTP headers via the use of the
ShibUseHeaders setting. Shibboleth documentation says the ShibUseHeaders
option should be avoided as they're not confident in the protections
against spoofed HTTP header attacks.

Running Webwork under mod_perl again so we don't need to use mod_proxy.
This resulted in hard to debug issues with a few sections of async code.
While I was able to 'unasync' those sections to get the thing running,
it feels unmaintainable in the long run, especially if Webwork increases
usage of async features.
  • Loading branch information
ionparticle committed Aug 14, 2024
1 parent 629f1c9 commit 753b969
Show file tree
Hide file tree
Showing 20 changed files with 2,517 additions and 0 deletions.
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ RUN cpanm install -n \
DBD::MariaDB \
Perl::Tidy@20220613 \
Archive::Zip::SimpleZip \
Net::SAML2 \
&& rm -fr ./cpanm /root/.cpanm /tmp/*

# ==================================================================
Expand Down
1 change: 1 addition & 0 deletions DockerfileStage1
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ RUN cpanm install -n \
DBD::MariaDB \
Perl::Tidy@20220613 \
Archive::Zip::SimpleZip \
Net::SAML2 \
&& rm -fr ./cpanm /root/.cpanm /tmp/*

# ==================================================================
120 changes: 120 additions & 0 deletions conf/authen_saml2.dist.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
---
################################################################################
# Configuration for the Saml2 plugin
#
# To enable the Saml2 plugin, copy authen_saml2.dist.yml to authen_saml2.yml
#
# The Saml2 plugin uses the Net::SAML2 library, the library claims to be
# compatible with a wide range of SAML2 implementations, including Shibboleth.
################################################################################

# add this query to the end of a course url to skip the saml2 authen module
# and go to the next one, comment out to disable this feature
bypass_query: bypassSaml2
idp: # this is the central SAML2 server
# url where we can get the SAML2 metadata xml for the IdP
metadata_url: http://idp.docker:8180/simplesaml/module.php/saml/idp/metadata
sp: # this is the Webwork side
# also known as iss (issuer)
entity_id: http://localhost:8080/saml2
# endoints created by the plugin, relative to the webwork root url
route:
base: '/saml2' # prefix path for all URLs handled by the plugin
metadata: '/metadata' # actual path would be /saml2/metadata
# 'Assertion Consumer Service', basically handles the SAML response, plugin
# only supports a POST response (HTTP POST Binding)
acs:
post: '/acs/post'
# Ideally, there would be a way to have separate app info and org info but
# Net::SAML2's metadata generation doesn't seem to have that separation. So
# I've filled out the org info with app info instead.
org:
contact: '[email protected]'
name: 'webwork'
url: 'https://localhost:8080/'
display_name: 'WeBWorK'
# list of attributes that can be used as the username, each of them will be
# tried in turn to see if there's a matching user in the classlist. If no
# attributes are given, then we'll default to the NameID. Please use the
# attribute's OID
attributes:
- 'urn:oid:0.9.2342.19200300.100.1.1'
##############################################################################
# SECURITY WARNING
# For production, you MUST generate your own unique 'cert' and 'signing_key'.
# The examples below are publicly exposed and thus provides NO SECURITY.
##############################################################################
# Cert and key pairs can be generated using an openssl command such as:
# openssl req -newkey rsa:3072 -new -x509 -days 3652 -nodes -out webwork.crt -keyout webwork.pem
# Where webwork.crt contains the cert and webwork.pem contains the signing_key
cert: |
-----BEGIN CERTIFICATE-----
MIIE7zCCA1egAwIBAgIUIteyNYLSAiB0FcNl0GLJNYRppk8wDQYJKoZIhvcNAQEL
BQAwgYYxCzAJBgNVBAYTAkFBMQswCQYDVQQIDAJBQTEQMA4GA1UEBwwHRXhhbXBs
ZTEQMA4GA1UECgwHRXhhbXBsZTEQMA4GA1UECwwHRXhhbXBsZTEQMA4GA1UEAwwH
RXhhbXBsZTEiMCAGCSqGSIb3DQEJARYTZXhhbXBsZUBleGFtcGxlLmVkdTAeFw0y
NDA1MDMwMTA2MzNaFw0zNDA1MDMwMTA2MzNaMIGGMQswCQYDVQQGEwJBQTELMAkG
A1UECAwCQUExEDAOBgNVBAcMB0V4YW1wbGUxEDAOBgNVBAoMB0V4YW1wbGUxEDAO
BgNVBAsMB0V4YW1wbGUxEDAOBgNVBAMMB0V4YW1wbGUxIjAgBgkqhkiG9w0BCQEW
E2V4YW1wbGVAZXhhbXBsZS5lZHUwggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGK
AoIBgQC45DCHUejzAeq+eVwEX5zSQWC+kqydEmoxpydT4YiSXnNeoNAkilKfHGOY
Uc4djwx148N14A+S0GCys2j3Ey2wuL7DSep5y1Z9Uxj6Ayg23XGIFFFJJLMy1Qfe
pjCcr1djPH9PpwglG1nTsiWqvHGGc3WWn1u6RfyrCf+jxbhygNRTA+LVPpqNvko6
MWKsbVLKrYMV2kPcQ0PQByNHJnjBy3KH2k99lS20h32sgHbgbVpJWdAWjeyJrOh9
aDt4/AfK90BvhkjF4BuQ+Jw5oIwMhbx7YmzIfiJmBLLaGjVRppuoAQtLX9uLst9l
aLZzaeutg+G3RUYcvDMlnP7cU8Sq4BD7uK0ChKxxCMFcihAhQ8wqCKncaaE9WqPs
CM16SB/6xptOxoLcg/5q3PJyUi2g4VDKXuQc6AURKIJxSM9nlrcv/R7fCFgk3Nj/
piWykDk6/BDWFpEHaj+NnFE9ZIxKr9CjTxdmqiDTyqSv50rNCjleyL/iASBTSCCF
OPVOYQECAwEAAaNTMFEwHQYDVR0OBBYEFGs8F3VIGSEk+DE2MBqqNKX6UuZTMB8G
A1UdIwQYMBaAFGs8F3VIGSEk+DE2MBqqNKX6UuZTMA8GA1UdEwEB/wQFMAMBAf8w
DQYJKoZIhvcNAQELBQADggGBAIpDktpfGH7ZqgdWvxbJrjekb1IyCGrWsHOYSjwM
+MxnhAA6oY63wC04a2i31zIMNOkY9F0tAdd4uDchxA9IWHqpb7t7zBlZdDabPPC3
WoDYnKhtZBULVVo7AvWO0UJGfZNJE393aKer3ePvfoG0OpCyrw4eFI/GCd4UjJBF
DnD7hvUxE7RRwOhbuYrtDRuB3Z7CeeP8o81eDVexyuBpM/9UQjYPqBBAfoeYKQzu
ZIhpGRWXw0ntH+EEOWagRXA5pRru61hteParZe4LBjPqisqN4Ek6ZR7MD9gB5xnt
Pn1BKRY08quFOZyaogzwfkYk5SCF8F8jBA8ZNAYwJWe1gtO3iw5vpUaQc2iCabvI
Y+Pc6qsSNwbkl7+sFrVHzI9QZVyz1cARUXxvrgGNLBkYtprkG91k6mCjX90cQspb
ZwHixcQyCNv+4H738e99h/Wf0YzjxFjDKrbGoosYBzWAsYYtzrtsBvw3SJMTXIh7
OvFMA+rbIL8XWs8oNmZDDh8g0A==
-----END CERTIFICATE-----
signing_key: |
-----BEGIN PRIVATE KEY-----
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
dzDq+xUD8nHpKM33A2EaUFY=
-----END PRIVATE KEY-----
42 changes: 42 additions & 0 deletions docker-config/docker-compose.dist.yml
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,48 @@ services:
#ports:
# - "6311:6311"

# saml2 dev use only, separate profile from the other services so it doesn't
# start in normal usage. Use "docker compose --profile saml2dev up" to start.
idp:
build:
context: ./docker-config/idp/ # SimpleSAMLphp based IDP
profiles:
- saml2dev
ports:
- '8180:80'
environment:
SP_METADATA_URL: 'http://app.docker:8080/saml2/metadata'
# the healthcheck url is simplesamlphp's url for triggering cron jobs, the
# cron job it'll trigger is to automatically grab webwork sp's metadata
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost/simplesaml/module.php/cron/run/docker/healthcheck']
start_period: 1m
start_interval: 15s
interval: 1h
retries: 1
timeout: 10s
# Send internal docker traffic for the idp external port to the idp internal
# port. Needed so the webwork saml2 plugin can request the idp metadata.
socat-idp:
image: alpine/socat:1.8.0.0
profiles:
- saml2dev
command: 'TCP-LISTEN:8180,fork,reuseaddr TCP:idp:80'
networks:
default:
aliases:
- idp.docker
# same port redirect so the idp can get the webwork saml2 plugin metadata
socat-app:
image: alpine/socat:1.8.0.0
profiles:
- saml2dev
command: 'TCP-LISTEN:8080,fork,reuseaddr TCP:app:8080'
networks:
default:
aliases:
- app.docker

volumes:
oplVolume:
driver: local
Expand Down
36 changes: 36 additions & 0 deletions docker-config/idp/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# actual image we'll run in the end
FROM php:8.3-apache
WORKDIR /var/www

# Install composer & php extension installer
COPY --from=composer/composer:2-bin /composer /usr/bin/composer
COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/

RUN apt-get update && \
apt-get -y install git curl vim && \
install-php-extensions ldap zip

# dirs used by simplesamlphp needs to be accessible by apache user
RUN mkdir simplesamlphp/ /var/cache/simplesamlphp
RUN chown www-data. simplesamlphp/ /var/cache/simplesamlphp
# Composer doesn't like to be root, so we'll run the rest as the apache user
USER www-data

# Install simplesamlphp
ARG SIMPLESAMLPHP_TAG=v2.2.1
RUN git clone --branch $SIMPLESAMLPHP_TAG https://github.com/simplesamlphp/simplesamlphp.git
WORKDIR /var/www/simplesamlphp

# Generate certs
RUN cd cert/ && \
openssl req -newkey rsa:3072 -new -x509 -days 3652 -nodes -out server.crt -keyout server.pem -subj "/C=CA/SP=BC/L=Vancouver/O=UBC/CN=idp.docker"

# Use composer to install dependencies
RUN composer install && \
composer require simplesamlphp/simplesamlphp-module-metarefresh

# Copy config files
COPY ./config/ config/
COPY ./metadata/ metadata/

COPY ./apache.conf /etc/apache2/sites-available/000-default.conf
38 changes: 38 additions & 0 deletions docker-config/idp/apache.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<VirtualHost *:80>
# The ServerName directive sets the request scheme, hostname and port that
# the server uses to identify itself. This is used when creating
# redirection URLs. In the context of virtual hosts, the ServerName
# specifies what hostname must appear in the request's Host: header to
# match this virtual host. For the default virtual host (this file) this
# value is not decisive as it is used as a last resort host regardless.
# However, you must set it for any further virtual host explicitly.
#ServerName www.example.com

ServerAdmin webmaster@localhost
DocumentRoot /var/www/simplesamlphp

# Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
# error, crit, alert, emerg.
# It is also possible to configure the loglevel for particular
# modules, e.g.
#LogLevel info ssl:warn

ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined

# For most configuration files from conf-available/, which are
# enabled or disabled at a global level, it is possible to
# include a line for only one particular virtual host. For example the
# following line enables the CGI configuration for this host only
# after it has been globally disabled with "a2disconf".
#Include conf-available/serve-cgi-bin.conf

SetEnv SIMPLESAMLPHP_CONFIG_DIR /var/www/simplesamlphp/config

Alias /simplesaml /var/www/simplesamlphp/public

<Directory /var/www/simplesamlphp/public>
Require all granted
</Directory>

</VirtualHost>
Loading

0 comments on commit 753b969

Please sign in to comment.