diff --git a/Dockerfile b/Dockerfile
index 45fe592463..67d6fcb305 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -33,7 +33,7 @@ RUN echo Cloning branch $PG_BRANCH branch from $PG_GIT_URL \
FROM ubuntu:24.04
ENV WEBWORK_URL=/webwork2 \
- WEBWORK_ROOT_URL=http://localhost::8080 \
+ WEBWORK_ROOT_URL=http://localhost:8080 \
WEBWORK_SMTP_SERVER=localhost \
WEBWORK_SMTP_SENDER=webwork@example.com \
WEBWORK_TIMEZONE=America/New_York \
@@ -190,6 +190,7 @@ RUN cpanm install -n \
DBD::MariaDB \
Perl::Tidy@20220613 \
Archive::Zip::SimpleZip \
+ Net::SAML2 \
&& rm -fr ./cpanm /root/.cpanm /tmp/*
# ==================================================================
diff --git a/DockerfileStage1 b/DockerfileStage1
index 2d4f569790..092b450ee9 100644
--- a/DockerfileStage1
+++ b/DockerfileStage1
@@ -152,6 +152,7 @@ RUN cpanm install -n \
DBD::MariaDB \
Perl::Tidy@20220613 \
Archive::Zip::SimpleZip \
+ Net::SAML2 \
&& rm -fr ./cpanm /root/.cpanm /tmp/*
# ==================================================================
diff --git a/DockerfileStage2 b/DockerfileStage2
index 59c6828e48..ddd48c17d9 100644
--- a/DockerfileStage2
+++ b/DockerfileStage2
@@ -36,7 +36,7 @@ RUN echo Cloning branch $PG_BRANCH branch from $PG_GIT_URL \
FROM webwork-base:forWW219
ENV WEBWORK_URL=/webwork2 \
- WEBWORK_ROOT_URL=http://localhost::8080 \
+ WEBWORK_ROOT_URL=http://localhost:8080 \
WEBWORK_SMTP_SERVER=localhost \
WEBWORK_SMTP_SENDER=webwork@example.com \
WEBWORK_TIMEZONE=America/New_York \
diff --git a/conf/authen_saml2.dist.yml b/conf/authen_saml2.dist.yml
new file mode 100644
index 0000000000..6200516132
--- /dev/null
+++ b/conf/authen_saml2.dist.yml
@@ -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: 'webwork@example.edu'
+ 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-----
+ MIIG/QIBADANBgkqhkiG9w0BAQEFAASCBucwggbjAgEAAoIBgQC45DCHUejzAeq+
+ eVwEX5zSQWC+kqydEmoxpydT4YiSXnNeoNAkilKfHGOYUc4djwx148N14A+S0GCy
+ s2j3Ey2wuL7DSep5y1Z9Uxj6Ayg23XGIFFFJJLMy1QfepjCcr1djPH9PpwglG1nT
+ siWqvHGGc3WWn1u6RfyrCf+jxbhygNRTA+LVPpqNvko6MWKsbVLKrYMV2kPcQ0PQ
+ ByNHJnjBy3KH2k99lS20h32sgHbgbVpJWdAWjeyJrOh9aDt4/AfK90BvhkjF4BuQ
+ +Jw5oIwMhbx7YmzIfiJmBLLaGjVRppuoAQtLX9uLst9laLZzaeutg+G3RUYcvDMl
+ nP7cU8Sq4BD7uK0ChKxxCMFcihAhQ8wqCKncaaE9WqPsCM16SB/6xptOxoLcg/5q
+ 3PJyUi2g4VDKXuQc6AURKIJxSM9nlrcv/R7fCFgk3Nj/piWykDk6/BDWFpEHaj+N
+ nFE9ZIxKr9CjTxdmqiDTyqSv50rNCjleyL/iASBTSCCFOPVOYQECAwEAAQKCAYAR
+ p6iCo22tFrfFrGz+9epRoXCNgg/9h66gQyfcOKMD5wT5Oj3l31d4XgucleMqq2gz
+ MaaOcPDLwh4ZskwJm8k3IM0GdN5w9tuxZ+fwp7CFXKvkpJwGcfyyk+kGd7QYoh2k
+ GjjF8Fs0v+HZ9x7lqMzmW8wUr+7gYKJ56qCAkPbF6EteCfb1Cd9UPaF04RZdBKtt
+ MxhbU9Y7CClHigbyWlgZmUW8dzoz8bTFklKL0FCJqad/bZYTMUYu91XT88oKCXbD
+ AUxpF2Ikbkfj820XOqq8iV3xGpYszt1aMRpsdXbDAhCqfKoNet2X7jnRWlNXZutC
+ RIUGm4VUNDNeD4nXW8aLgDa8bNQnvsSmM9DUVuPjbejUs0VN7uwxo8rYqvkAKiBQ
+ 1ZqxoBK4ShZVcqgWE6CUj9FRZ3CVzSzydxZSQzex/ZRYPuYLUhQJFHLVIdJSYhf3
+ XTEki0+ndwAB7yP/tBNlcxLftCzAaS7mPLLn1tf0A27QPCSjwOsTLxuJ4WYVkmkC
+ gcEAuuh8EImBfE9WOg3ITmJpr95WlVi8WE6BHWowV8dQwODQLj+38itDDL1xLn9+
+ Vuz4o9AaIBiH5fCr6otun28lVp/sNVdWnBVeioSpu3tGV18OiDNaXtXOo7qkUnBI
+ Z+V7cD69gJLS6byD3OXlGi42h3XxK4mVlhwQtkQ69qI/zhl6rc0O2/iXXUAFa5T5
+ MJ84Cw1B9kHFB/NC27sraee+cwAK0Pogj5WnqaBOIPeIO/f+br65xMUvEYvDD1m4
+ TwIzAoHBAP082l0IQ5KHBY4WuFIDOoevO5SxHN5EUp2sPRDZwZxwOrjHxFRXPc/h
+ pDrVEHEn/4HQ706AHYpED0diumr4gee7gusNIDcGpXwjGVdFmFvxKoDbhz1C5vL3
+ xC7qgyS/ZtAopxpCPH3+7IrQyBk8e6He+8F97bA0e9sYSBQSuPLcdKQXGNbLYb6s
+ yLbP02cB2CNeI1GJMQIOXe9bi9Cz5w+hCGMvEKt5oAz5SLWlPBvv1YATpG5Ux8Wy
+ RbGPD4zj+wKBwBGVDx6rIMAl4nGhnEcrYM/HdZOk/kq8T88JjzSirkkGnO7M1av1
+ P+Bx7bS3D5Zzwkv+poaAaEBMLI/qv+RFm1iTwK+f4KjcJcGYCzN0vEA50+8iDY1A
+ RakHRK/wmg8T+lGrxT3UEf0k266q/atBz6VchexXi/fL+hJ7RqSuzJvBr9WrpYsx
+ zmNaQ2hEYlCdmbMIcz0MINHHo3FyIPpcb4D37wyLiwaWyGffiZn2Tx19DbUzQdxt
+ xCi9YgMOqJTeGwKBwQD4rJ0x5j+U0ApgcWcnAgyj2SwE47eZfDY0p0KAHZXGbV78
+ vQ7KU7FbRhTjwP6YX9LEQ8v7pktbz2HBk+3DxayrRrNU5lrQLjKrKDxmOu1WvAgk
+ 6W5wdhYcWbnI6HlHyLzJhGIzov+MKp1V45fbUE2Hs1Q9uc+CzMcja0C8lXYQ5vOT
+ fyrhIm8lsr6W5paN/H2mnXbJRpNdlYYg2iD+HOu1qUh3PWx9Nr44f0MrPMs+E9Hw
+ J1m9DnvuYxWVOwrmK6kCgcADfcatftIJWMqeYJsDnB9jJaANmjln2G3bppo9WcIC
+ lvfXFE+Rf3FleaijVrUFbgxDU2MHh/2VPjJgIQT3QtfqS5+OnF1Z5+uOTGwbDNmT
+ 3Th0IcSt6TjvLJwkanNeSkvc+2lMnuNtH6TQLXB0qEs3D7xND0kFWHfyies+RYNC
+ eualoZJ/6UL9X2gkPG5jmzXjInEBguAL0ll5yETXgx6v0hXR058TcvPl58j73cCQ
+ dzDq+xUD8nHpKM33A2EaUFY=
+ -----END PRIVATE KEY-----
diff --git a/docker-config/docker-compose.dist.yml b/docker-config/docker-compose.dist.yml
index c5d51e06b9..a168a9f96e 100644
--- a/docker-config/docker-compose.dist.yml
+++ b/docker-config/docker-compose.dist.yml
@@ -1,4 +1,3 @@
-version: '3.5'
services:
db:
image: mariadb:10.4
@@ -252,6 +251,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
diff --git a/docker-config/idp/Dockerfile b/docker-config/idp/Dockerfile
new file mode 100644
index 0000000000..a9dac13770
--- /dev/null
+++ b/docker-config/idp/Dockerfile
@@ -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
diff --git a/docker-config/idp/apache.conf b/docker-config/idp/apache.conf
new file mode 100644
index 0000000000..bbe3dc3eed
--- /dev/null
+++ b/docker-config/idp/apache.conf
@@ -0,0 +1,38 @@
+
+ # 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
+
+
+ Require all granted
+
+
+
diff --git a/docker-config/idp/config/authsources.php b/docker-config/idp/config/authsources.php
new file mode 100644
index 0000000000..608ab59c30
--- /dev/null
+++ b/docker-config/idp/config/authsources.php
@@ -0,0 +1,355 @@
+ [
+ // The default is to use core:AdminPassword, but it can be replaced with
+ // any authentication source.
+
+ 'core:AdminPassword',
+ ],
+
+
+ // An authentication source which can authenticate against SAML 2.0 IdPs.
+ //'default-sp' => [
+ // 'saml:SP',
+
+ // // The entity ID of this SP.
+ // 'entityID' => 'https://myapp.example.org/',
+
+ // // The entity ID of the IdP this SP should contact.
+ // // Can be NULL/unset, in which case the user will be shown a list of available IdPs.
+ // 'idp' => null,
+
+ // // The URL to the discovery service.
+ // // Can be NULL/unset, in which case a builtin discovery service will be used.
+ // 'discoURL' => null,
+
+ // /*
+ // * If SP behind the SimpleSAMLphp in IdP/SP proxy mode requests
+ // * AuthnContextClassRef, decide whether the AuthnContextClassRef will be
+ // * processed by the IdP/SP proxy or if it will be passed to the original
+ // * IdP in front of the IdP/SP proxy.
+ // */
+ // 'proxymode.passAuthnContextClassRef' => false,
+
+ // /*
+ // * The attributes parameter must contain an array of desired attributes by the SP.
+ // * The attributes can be expressed as an array of names or as an associative array
+ // * in the form of 'friendlyName' => 'name'. This feature requires 'name' to be set.
+ // * The metadata will then be created as follows:
+ // *
+ // */
+ // /*
+ // 'name' => [
+ // 'en' => 'A service',
+ // 'no' => 'En tjeneste',
+ // ],
+
+ // 'attributes' => [
+ // 'attrname' => 'urn:oid:x.x.x.x',
+ // ],
+ // 'attributes.required' => [
+ // 'urn:oid:x.x.x.x',
+ // ],
+ // */
+ //],
+
+
+ /*
+ 'example-sql' => [
+ 'sqlauth:SQL',
+ 'dsn' => 'pgsql:host=sql.example.org;port=5432;dbname=simplesaml',
+ 'username' => 'simplesaml',
+ 'password' => 'secretpassword',
+ 'query' => 'SELECT uid, givenName, email, eduPersonPrincipalName FROM users WHERE uid = :username ' .
+ 'AND password = SHA2(CONCAT((SELECT salt FROM users WHERE uid = :username), :password), 256);',
+ ],
+ */
+
+ /*
+ 'example-static' => [
+ 'exampleauth:StaticSource',
+ 'uid' => ['testuser'],
+ 'eduPersonAffiliation' => ['member', 'employee'],
+ 'cn' => ['Test User'],
+ ],
+ */
+
+ 'example-userpass' => [
+ 'exampleauth:UserPass',
+
+ // Give the user an option to save their username for future login attempts
+ // And when enabled, what should the default be, to save the username or not
+ //'remember.username.enabled' => false,
+ //'remember.username.checked' => false,
+
+ 'users' => [
+ 'student01:student01' => [
+ 'uid' => ['student01'],
+ 'displayName' => 'Student 01',
+ 'eduPersonAffiliation' => ['student'],
+ 'mail' => 'student01@example.edu'
+ ],
+ 'instructor01:instructor01' => [
+ 'uid' => ['instructor01'],
+ 'displayName' => 'Instructor 01',
+ 'alt' => '51092d7f-2f38-4a91-bfb0-13a021c02df3',
+ 'eduPersonAffiliation' => ['faculty', 'student'],
+ 'mail' => 'instructor01@example.edu'
+ ],
+ 'staff01:staff01' => [
+ 'uid' => ['staff01'],
+ 'displayName' => 'Staff 01',
+ 'eduPersonAffiliation' => ['staff', 'alumni'],
+ 'mail' => 'staff01@example.edu'
+ ],
+ ],
+ ],
+
+ /*
+ 'crypto-hash' => [
+ 'authcrypt:Hash',
+ // hashed version of 'verysecret', made with bin/pwgen.php
+ 'professor:{SSHA256}P6FDTEEIY2EnER9a6P2GwHhI5JDrwBgjQ913oVQjBngmCtrNBUMowA==' => [
+ 'uid' => ['prof_a'],
+ 'eduPersonAffiliation' => ['member', 'employee', 'board'],
+ ],
+ ],
+ */
+
+ /*
+ 'htpasswd' => [
+ 'authcrypt:Htpasswd',
+ 'htpasswd_file' => '/var/www/foo.edu/legacy_app/.htpasswd',
+ 'static_attributes' => [
+ 'eduPersonAffiliation' => ['member', 'employee'],
+ 'Organization' => ['University of Foo'],
+ ],
+ ],
+ */
+
+ /*
+ // This authentication source serves as an example of integration with an
+ // external authentication engine. Take a look at the comment in the beginning
+ // of modules/exampleauth/lib/Auth/Source/External.php for a description of
+ // how to adjust it to your own site.
+ 'example-external' => [
+ 'exampleauth:External',
+ ],
+ */
+
+ /*
+ 'yubikey' => [
+ 'authYubiKey:YubiKey',
+ 'id' => '000',
+ // 'key' => '012345678',
+ ],
+ */
+
+ /*
+ 'facebook' => [
+ 'authfacebook:Facebook',
+ // Register your Facebook application on http://www.facebook.com/developers
+ // App ID or API key (requests with App ID should be faster; https://github.com/facebook/php-sdk/issues/214)
+ 'api_key' => 'xxxxxxxxxxxxxxxx',
+ // App Secret
+ 'secret' => 'xxxxxxxxxxxxxxxx',
+ // which additional data permissions to request from user
+ // see http://developers.facebook.com/docs/authentication/permissions/ for the full list
+ // 'req_perms' => 'email,user_birthday',
+ // Which additional user profile fields to request.
+ // When empty, only the app-specific user id and name will be returned
+ // See https://developers.facebook.com/docs/graph-api/reference/v2.6/user for the full list
+ // 'user_fields' => 'email,birthday,third_party_id,name,first_name,last_name',
+ ],
+ */
+
+ /*
+ // Twitter OAuth Authentication API.
+ // Register your application to get an API key here:
+ // http://twitter.com/oauth_clients
+ 'twitter' => [
+ 'authtwitter:Twitter',
+ 'key' => 'xxxxxxxxxxxxxxxx',
+ 'secret' => 'xxxxxxxxxxxxxxxx',
+ // Forces the user to enter their credentials to ensure the correct users account is authorized.
+ // Details: https://dev.twitter.com/docs/api/1/get/oauth/authenticate
+ 'force_login' => false,
+ ],
+ */
+
+ /*
+ // Microsoft Account (Windows Live ID) Authentication API.
+ // Register your application to get an API key here:
+ // https://apps.dev.microsoft.com/
+ 'windowslive' => [
+ 'authwindowslive:LiveID',
+ 'key' => 'xxxxxxxxxxxxxxxx',
+ 'secret' => 'xxxxxxxxxxxxxxxx',
+ ],
+ */
+
+ /*
+ // Example of a LDAP authentication source.
+ 'example-ldap' => [
+ 'ldap:Ldap',
+
+ // The connection string for the LDAP-server.
+ // You can add multiple by separating them with a space.
+ 'connection_string' => 'ldap.example.org',
+
+ // Whether SSL/TLS should be used when contacting the LDAP server.
+ // Possible values are 'ssl', 'tls' or 'none'
+ 'encryption' => 'ssl',
+
+ // The LDAP version to use when interfacing the LDAP-server.
+ // Defaults to 3
+ 'version' => 3,
+
+ // Set to TRUE to enable LDAP debug level. Passed to the LDAP connector class.
+ //
+ // Default: FALSE
+ // Required: No
+ 'ldap.debug' => false,
+
+ // The LDAP-options to pass when setting up a connection
+ // See [Symfony documentation][1]
+ 'options' => [
+
+ // Set whether to follow referrals.
+ // AD Controllers may require 0x00 to function.
+ // Possible values are 0x00 (NEVER), 0x01 (SEARCHING),
+ // 0x02 (FINDING) or 0x03 (ALWAYS).
+ 'referrals' => 0x00,
+
+ 'network_timeout' => 3,
+ ],
+
+ // The connector to use.
+ // Defaults to '\SimpleSAML\Module\ldap\Connector\Ldap', but can be set
+ // to '\SimpleSAML\Module\ldap\Connector\ActiveDirectory' when
+ // authenticating against Microsoft Active Directory. This will
+ // provide you with more specific error messages.
+ 'connector' => '\SimpleSAML\Module\ldap\Connector\Ldap',
+
+ // Which attributes should be retrieved from the LDAP server.
+ // This can be an array of attribute names, or NULL, in which case
+ // all attributes are fetched.
+ 'attributes' => null,
+
+ // Which attributes should be base64 encoded after retrieval from
+ // the LDAP server.
+ 'attributes.binary' => [
+ 'jpegPhoto',
+ 'objectGUID',
+ 'objectSid',
+ 'mS-DS-ConsistencyGuid'
+ ],
+
+ // The pattern which should be used to create the user's DN given
+ // the username. %username% in this pattern will be replaced with
+ // the user's username.
+ //
+ // This option is not used if the search.enable option is set to TRUE.
+ 'dnpattern' => 'uid=%username%,ou=people,dc=example,dc=org',
+
+ // As an alternative to specifying a pattern for the users DN, it is
+ // possible to search for the username in a set of attributes. This is
+ // enabled by this option.
+ 'search.enable' => false,
+
+ // An array on DNs which will be used as a base for the search. In
+ // case of multiple strings, they will be searched in the order given.
+ 'search.base' => [
+ 'ou=people,dc=example,dc=org',
+ ],
+
+ // The scope of the search. Valid values are 'sub' and 'one' and
+ // 'base', first one being the default if no value is set.
+ 'search.scope' => 'sub',
+
+ // The attribute(s) the username should match against.
+ //
+ // This is an array with one or more attribute names. Any of the
+ // attributes in the array may match the value the username.
+ 'search.attributes' => ['uid', 'mail'],
+
+ // Additional filters that must match for the entire LDAP search to
+ // be true.
+ //
+ // This should be a single string conforming to [RFC 1960][2]
+ // and [RFC 2544][3]. The string is appended to the search attributes
+ 'search.filter' => '(&(objectClass=Person)(|(sn=Doe)(cn=John *)))',
+
+ // The username & password where SimpleSAMLphp should bind to before
+ // searching. If this is left NULL, no bind will be performed before
+ // searching.
+ 'search.username' => null,
+ 'search.password' => null,
+ ],
+ */
+
+ /*
+ // Example of an LDAPMulti authentication source.
+ 'example-ldapmulti' => [
+ 'ldap:LdapMulti',
+
+ // The way the organization as part of the username should be handled.
+ // Three possible values:
+ // - 'none': No handling of the organization. Allows '@' to be part
+ // of the username.
+ // - 'allow': Will allow users to type 'username@organization'.
+ // - 'force': Force users to type 'username@organization'. The dropdown
+ // list will be hidden.
+ //
+ // The default is 'none'.
+ 'username_organization_method' => 'none',
+
+ // Whether the organization should be included as part of the username
+ // when authenticating. If this is set to TRUE, the username will be on
+ // the form @. If this is FALSE, the
+ // username will be used as the user enters it.
+ //
+ // The default is FALSE.
+ 'include_organization_in_username' => false,
+
+ // A list of available LDAP servers.
+ //
+ // The index is an identifier for the organization/group. When
+ // 'username_organization_method' is set to something other than 'none',
+ // the organization-part of the username is matched against the index.
+ //
+ // The value of each element is an array in the same format as an LDAP
+ // authentication source.
+ 'mapping' => [
+ 'employees' => [
+ // A short name/description for this group. Will be shown in a
+ // dropdown list when the user logs on.
+ //
+ // This option can be a string or an array with
+ // language => text mappings.
+ 'description' => 'Employees',
+ 'authsource' => 'example-ldap',
+ ],
+
+ 'students' => [
+ 'description' => 'Students',
+ 'authsource' => 'example-ldap-2',
+ ],
+ ],
+ ],
+ */
+];
diff --git a/docker-config/idp/config/config.php b/docker-config/idp/config/config.php
new file mode 100644
index 0000000000..b42a1e77f5
--- /dev/null
+++ b/docker-config/idp/config/config.php
@@ -0,0 +1,1303 @@
+ 'simplesaml/',
+
+ /*
+ * The 'application' configuration array groups a set configuration options
+ * relative to an application protected by SimpleSAMLphp.
+ */
+ 'application' => [
+ /*
+ * The 'baseURL' configuration option allows you to specify a protocol,
+ * host and optionally a port that serves as the canonical base for all
+ * your application's URLs. This is useful when the environment
+ * observed in the server differs from the one observed by end users,
+ * for example, when using a load balancer to offload TLS.
+ *
+ * Note that this configuration option does not allow setting a path as
+ * part of the URL. If your setup involves URL rewriting or any other
+ * tricks that would result in SimpleSAMLphp observing a URL for your
+ * application's scripts different than the canonical one, you will
+ * need to compute the right URLs yourself and pass them dynamically
+ * to SimpleSAMLphp's API.
+ */
+ //'baseURL' => 'https://example.com',
+ ],
+
+ /*
+ * The following settings are *filesystem paths* which define where
+ * SimpleSAMLphp can find or write the following things:
+ * - 'cachedir': Where SimpleSAMLphp can write its cache.
+ * - 'loggingdir': Where to write logs. MUST be set to NULL when using a logging
+ * handler other than `file`.
+ * - 'datadir': Storage of general data.
+ * - 'tempdir': Saving temporary files. SimpleSAMLphp will attempt to create
+ * this directory if it doesn't exist. DEPRECATED - replaced by cachedir.
+ * When specified as a relative path, this is relative to the SimpleSAMLphp
+ * root directory.
+ */
+ 'cachedir' => '/var/cache/simplesamlphp',
+ //'loggingdir' => '/var/log/',
+ //'datadir' => '/var/data/',
+ //'tempdir' => '/tmp/simplesamlphp',
+
+ /*
+ * Certificate and key material can be loaded from different possible
+ * locations. Currently two locations are supported, the local filesystem
+ * and the database via pdo using the global database configuration. Locations
+ * are specified by a URL-link prefix before the file name/path or database
+ * identifier.
+ */
+
+ /* To load a certificate or key from the filesystem, it should be specified
+ * as 'file://' where is either a relative filename or a fully
+ * qualified path to a file containing the certificate or key in PEM
+ * format, such as 'cert.pem' or '/path/to/cert.pem'. If the path is
+ * relative, it will be searched for in the directory defined by the
+ * 'certdir' parameter below. When 'certdir' is specified as a relative
+ * path, it will be interpreted as relative to the SimpleSAMLphp root
+ * directory. Note that locations with no prefix included will be treated
+ * as file locations.
+ */
+ 'certdir' => 'cert/',
+
+ /* To load a certificate or key from the database, it should be specified
+ * as 'pdo://' where is the identifier in the database table that
+ * should be matched. While the certificate and key tables are expected to
+ * be in the simplesaml database, they are not created or managed by
+ * simplesaml. The following parameters control how the pdo location
+ * attempts to retrieve certificates and keys from the database:
+ *
+ * - 'cert.pdo.table': name of table where certificates are stored
+ * - 'cert.pdo.keytable': name of table where keys are stored
+ * - 'cert.pdo.apply_prefix': whether or not to prepend the database.prefix
+ * parameter to the table names; if you are using
+ * database.prefix to separate multiple SSP instances
+ * in the same database but want to share certificate/key
+ * data between them, set this to false
+ * - 'cert.pdo.id_column': name of column to use as identifier
+ * - 'cert.pdo.data_column': name of column where PEM data is stored
+ *
+ * Basically, the query executed will be:
+ *
+ * SELECT cert.pdo.data_column FROM cert.pdo.table WHERE cert.pdo.id_column = :id
+ *
+ * Defaults are shown below, to change them, uncomment the line and update as
+ * needed
+ */
+ //'cert.pdo.table' => 'certificates',
+ //'cert.pdo.keytable' => 'private_keys',
+ //'cert.pdo.apply_prefix' => true,
+ //'cert.pdo.id_column' => 'id',
+ //'cert.pdo.data_column' => 'data',
+
+ /*
+ * Some information about the technical persons running this installation.
+ * The email address will be used as the recipient address for error reports, and
+ * also as the technical contact in generated metadata.
+ */
+ 'technicalcontact_name' => 'Administrator',
+ 'technicalcontact_email' => 'na@example.org',
+
+ /*
+ * (Optional) The method by which email is delivered. Defaults to mail which utilizes the
+ * PHP mail() function.
+ *
+ * Valid options are: mail, sendmail and smtp.
+ */
+ //'mail.transport.method' => 'smtp',
+
+ /*
+ * Set the transport options for the transport method specified. The valid settings are relative to the
+ * selected transport method.
+ */
+ /*
+ 'mail.transport.options' => [
+ 'host' => 'mail.example.org', // required
+ 'port' => 25, // optional
+ 'username' => 'user@example.org', // optional: if set, enables smtp authentication
+ 'password' => 'password', // optional: if set, enables smtp authentication
+ 'security' => 'tls', // optional: defaults to no smtp security
+ 'smtpOptions' => [], // optional: passed to stream_context_create when connecting via SMTP
+ ],
+
+ // sendmail mail transport options
+ /*
+ 'mail.transport.options' => [
+ 'path' => '/usr/sbin/sendmail' // optional: defaults to php.ini path
+ ],
+ */
+
+ /*
+ * The envelope from address for outgoing emails.
+ * This should be in a domain that has your application's IP addresses in its SPF record
+ * to prevent it from being rejected by mail filters.
+ */
+ //'sendmail_from' => 'no-reply@example.org',
+
+ /*
+ * The timezone of the server. This option should be set to the timezone you want
+ * SimpleSAMLphp to report the time in. The default is to guess the timezone based
+ * on your system timezone.
+ *
+ * See this page for a list of valid timezones: http://php.net/manual/en/timezones.php
+ */
+ 'timezone' => 'America/Vancouver',
+
+
+
+ /**********************************
+ | SECURITY CONFIGURATION OPTIONS |
+ **********************************/
+
+ /*
+ * This is a secret salt used by SimpleSAMLphp when it needs to generate a secure hash
+ * of a value. It must be changed from its default value to a secret value. The value of
+ * 'secretsalt' can be any valid string of any length.
+ *
+ * A possible way to generate a random salt is by running the following command from a unix shell:
+ * LC_ALL=C tr -c -d '0123456789abcdefghijklmnopqrstuvwxyz' /dev/null;echo
+ */
+ 'secretsalt' => 'h6GwzJYCUrc9SgU57Coc7anTduvfnb8U',
+
+ /*
+ * This password must be kept secret, and modified from the default value 123.
+ * This password will give access to the installation page of SimpleSAMLphp with
+ * metadata listing and diagnostics pages.
+ * You can also put a hash here; run "bin/pwgen.php" to generate one.
+ */
+ 'auth.adminpassword' => 'admin',
+
+ /*
+ * Set this option to true if you want to require administrator password to access the metadata.
+ */
+ 'admin.protectmetadata' => false,
+
+ /*
+ * Set this option to false if you don't want SimpleSAMLphp to check for new stable releases when
+ * visiting the configuration tab in the web interface.
+ */
+ 'admin.checkforupdates' => false,
+
+ /*
+ * Array of domains that are allowed when generating links or redirects
+ * to URLs. SimpleSAMLphp will use this option to determine whether to
+ * to consider a given URL valid or not, but you should always validate
+ * URLs obtained from the input on your own (i.e. ReturnTo or RelayState
+ * parameters obtained from the $_REQUEST array).
+ *
+ * SimpleSAMLphp will automatically add your own domain (either by checking
+ * it dynamically, or by using the domain defined in the 'baseurlpath'
+ * directive, the latter having precedence) to the list of trusted domains,
+ * in case this option is NOT set to NULL. In that case, you are explicitly
+ * telling SimpleSAMLphp to verify URLs.
+ *
+ * Set to an empty array to disallow ALL redirects or links pointing to
+ * an external URL other than your own domain. This is the default behaviour.
+ *
+ * Set to NULL to disable checking of URLs. DO NOT DO THIS UNLESS YOU KNOW
+ * WHAT YOU ARE DOING!
+ *
+ * Example:
+ * 'trusted.url.domains' => ['sp.example.com', 'app.example.com'],
+ */
+ 'trusted.url.domains' => [],
+
+ /*
+ * Enable regular expression matching of trusted.url.domains.
+ *
+ * Set to true to treat the values in trusted.url.domains as regular
+ * expressions. Set to false to do exact string matching.
+ *
+ * If enabled, the start and end delimiters ('^' and '$') will be added to
+ * all regular expressions in trusted.url.domains.
+ */
+ 'trusted.url.regex' => false,
+
+ /*
+ * Enable secure POST from HTTPS to HTTP.
+ *
+ * If you have some SP's on HTTP and IdP is normally on HTTPS, this option
+ * enables secure POSTing to HTTP endpoint without warning from browser.
+ *
+ * For this to work, module.php/core/postredirect.php must be accessible
+ * also via HTTP on IdP, e.g. if your IdP is on
+ * https://idp.example.org/ssp/, then
+ * http://idp.example.org/ssp/module.php/core/postredirect.php must be accessible.
+ */
+ 'enable.http_post' => false,
+
+ /*
+ * Set the allowed clock skew between encrypting/decrypting assertions
+ *
+ * If you have a server that is constantly out of sync, this option
+ * allows you to adjust the allowed clock-skew.
+ *
+ * Allowed range: 180 - 300
+ * Defaults to 180.
+ */
+ 'assertion.allowed_clock_skew' => 180,
+
+ /*
+ * Set custom security headers. The defaults can be found in \SimpleSAML\Configuration::DEFAULT_SECURITY_HEADERS
+ *
+ * NOTE: When a header is already set on the response we will NOT overrule it and leave it untouched.
+ *
+ * Whenever you change any of these headers, make sure to validate your config by running your
+ * hostname through a security-test like https://en.internet.nl
+ 'headers.security' => [
+ 'Content-Security-Policy' => "default-src 'none'; frame-ancestors 'self'; object-src 'none'; script-src 'self'; style-src 'self'; font-src 'self'; connect-src 'self'; img-src 'self' data:; base-uri 'none'",
+ 'X-Frame-Options' => 'SAMEORIGIN',
+ 'X-Content-Type-Options' => 'nosniff',
+ 'Referrer-Policy' => 'origin-when-cross-origin',
+],
+ */
+
+
+ /************************
+ | ERRORS AND DEBUGGING |
+ ************************/
+
+ /*
+ * The 'debug' option allows you to control how SimpleSAMLphp behaves in certain
+ * situations where further action may be taken
+ *
+ * It can be left unset, in which case, debugging is switched off for all actions.
+ * If set, it MUST be an array containing the actions that you want to enable, or
+ * alternatively a hashed array where the keys are the actions and their
+ * corresponding values are booleans enabling or disabling each particular action.
+ *
+ * SimpleSAMLphp provides some pre-defined actions, though modules could add new
+ * actions here. Refer to the documentation of every module to learn if they
+ * allow you to set any more debugging actions.
+ *
+ * The pre-defined actions are:
+ *
+ * - 'saml': this action controls the logging of SAML messages exchanged with other
+ * entities. When enabled ('saml' is present in this option, or set to true), all
+ * SAML messages will be logged, including plaintext versions of encrypted
+ * messages.
+ *
+ * - 'backtraces': this action controls the logging of error backtraces so you
+ * can debug any possible errors happening in SimpleSAMLphp.
+ *
+ * - 'validatexml': this action allows you to validate SAML documents against all
+ * the relevant XML schemas. SAML 1.1 messages or SAML metadata parsed with
+ * the XML to SimpleSAMLphp metadata converter or the metaedit module will
+ * validate the SAML documents if this option is enabled.
+ *
+ * If you want to disable debugging completely, unset this option or set it to an
+ * empty array.
+ */
+ 'debug' => [
+ 'saml' => false,
+ 'backtraces' => true,
+ 'validatexml' => false,
+ ],
+
+ /*
+ * When 'showerrors' is enabled, all error messages and stack traces will be output
+ * to the browser.
+ *
+ * When 'errorreporting' is enabled, a form will be presented for the user to report
+ * the error to 'technicalcontact_email'.
+ */
+ 'showerrors' => true,
+ 'errorreporting' => true,
+
+ /*
+ * Custom error show function called from SimpleSAML\Error\Error::show.
+ * See docs/simplesamlphp-errorhandling.md for function code example.
+ *
+ * Example:
+ * 'errors.show_function' => ['SimpleSAML\Module\example\Error', 'show'],
+ */
+
+
+ /**************************
+ | LOGGING AND STATISTICS |
+ **************************/
+
+ /*
+ * Define the minimum log level to log. Available levels:
+ * - SimpleSAML\Logger::ERR No statistics, only errors
+ * - SimpleSAML\Logger::WARNING No statistics, only warnings/errors
+ * - SimpleSAML\Logger::NOTICE Statistics and errors
+ * - SimpleSAML\Logger::INFO Verbose logs
+ * - SimpleSAML\Logger::DEBUG Full debug logs - not recommended for production
+ *
+ * Choose logging handler.
+ *
+ * Options: [syslog,file,errorlog,stderr]
+ *
+ * If you set the handler to 'file', the directory specified in loggingdir above
+ * must exist and be writable for SimpleSAMLphp. If set to something else, set
+ * loggingdir above to 'null'.
+ */
+ 'logging.level' => SimpleSAML\Logger::NOTICE,
+ 'logging.handler' => 'syslog',
+
+ /*
+ * Specify the format of the logs. Its use varies depending on the log handler used (for instance, you cannot
+ * control here how dates are displayed when using the syslog or errorlog handlers), but in general the options
+ * are:
+ *
+ * - %date{}: the date and time, with its format specified inside the brackets. See the PHP documentation
+ * of the date() function for more information on the format. If the brackets are omitted, the standard
+ * format is applied. This can be useful if you just want to control the placement of the date, but don't care
+ * about the format.
+ *
+ * - %process: the name of the SimpleSAMLphp process. Remember you can configure this in the 'logging.processname'
+ * option below.
+ *
+ * - %level: the log level (name or number depending on the handler used).
+ *
+ * - %stat: if the log entry is intended for statistical purposes, it will print the string 'STAT ' (bear in mind
+ * the trailing space).
+ *
+ * - %trackid: the track ID, an identifier that allows you to track a single session.
+ *
+ * - %srcip: the IP address of the client. If you are behind a proxy, make sure to modify the
+ * $_SERVER['REMOTE_ADDR'] variable on your code accordingly to the X-Forwarded-For header.
+ *
+ * - %msg: the message to be logged.
+ *
+ */
+ //'logging.format' => '%date{M j H:i:s} %process %level %stat[%trackid] %msg',
+
+ /*
+ * Choose which facility should be used when logging with syslog.
+ *
+ * These can be used for filtering the syslog output from SimpleSAMLphp into its
+ * own file by configuring the syslog daemon.
+ *
+ * See the documentation for openlog (http://php.net/manual/en/function.openlog.php) for available
+ * facilities. Note that only LOG_USER is valid on windows.
+ *
+ * The default is to use LOG_LOCAL5 if available, and fall back to LOG_USER if not.
+ */
+ 'logging.facility' => defined('LOG_LOCAL5') ? constant('LOG_LOCAL5') : LOG_USER,
+
+ /*
+ * The process name that should be used when logging to syslog.
+ * The value is also written out by the other logging handlers.
+ */
+ 'logging.processname' => 'simplesamlphp',
+
+ /*
+ * Logging: file - Logfilename in the loggingdir from above.
+ */
+ 'logging.logfile' => 'simplesamlphp.log',
+
+ /*
+ * This is an array of outputs. Each output has at least a 'class' option, which
+ * selects the output.
+ */
+ 'statistics.out' => [
+ // Log statistics to the normal log.
+ /*
+ [
+ 'class' => 'core:Log',
+ 'level' => 'notice',
+ ],
+ */
+ // Log statistics to files in a directory. One file per day.
+ /*
+ [
+ 'class' => 'core:File',
+ 'directory' => '/var/log/stats',
+ ],
+ */
+ ],
+
+
+
+ /***********************
+ | PROXY CONFIGURATION |
+ ***********************/
+
+ /*
+ * Proxy to use for retrieving URLs.
+ *
+ * Example:
+ * 'proxy' => 'tcp://proxy.example.com:5100'
+ */
+ 'proxy' => null,
+
+ /*
+ * Username/password authentication to proxy (Proxy-Authorization: Basic)
+ * Example:
+ * 'proxy.auth' = 'myuser:password'
+ */
+ //'proxy.auth' => 'myuser:password',
+
+
+
+ /**************************
+ | DATABASE CONFIGURATION |
+ **************************/
+
+ /*
+ * This database configuration is optional. If you are not using
+ * core functionality or modules that require a database, you can
+ * skip this configuration.
+ */
+
+ /*
+ * Database connection string.
+ * Ensure that you have the required PDO database driver installed
+ * for your connection string.
+ */
+ 'database.dsn' => 'mysql:host=localhost;dbname=saml',
+
+ /*
+ * SQL database credentials
+ */
+ 'database.username' => 'simplesamlphp',
+ 'database.password' => 'secret',
+ 'database.options' => [],
+
+ /*
+ * (Optional) Table prefix
+ */
+ 'database.prefix' => '',
+
+ /*
+ * (Optional) Driver options
+ */
+ 'database.driver_options' => [],
+
+ /*
+ * True or false if you would like a persistent database connection
+ */
+ 'database.persistent' => false,
+
+ /*
+ * Database secondary configuration is optional as well. If you are only
+ * running a single database server, leave this blank. If you have
+ * a primary/secondary configuration, you can define as many secondary servers
+ * as you want here. Secondaries will be picked at random to be queried from.
+ *
+ * Configuration options in the secondary array are exactly the same as the
+ * options for the primary (shown above) with the exception of the table
+ * prefix and driver options.
+ */
+ 'database.secondaries' => [
+ /*
+ [
+ 'dsn' => 'mysql:host=mysecondary;dbname=saml',
+ 'username' => 'simplesamlphp',
+ 'password' => 'secret',
+ 'persistent' => false,
+ ],
+ */
+ ],
+
+
+
+ /*************
+ | PROTOCOLS |
+ *************/
+
+ /*
+ * Which functionality in SimpleSAMLphp do you want to enable. Normally you would enable only
+ * one of the functionalities below, but in some cases you could run multiple functionalities.
+ * In example when you are setting up a federation bridge.
+ */
+ 'enable.saml20-idp' => true,
+ 'enable.adfs-idp' => false,
+
+
+
+ /***********
+ | MODULES |
+ ***********/
+
+ /*
+ * Configuration for enabling/disabling modules. By default the 'core', 'admin' and 'saml' modules are enabled.
+ *
+ * Example:
+ *
+ * 'module.enable' => [
+ * 'exampleauth' => true, // Setting to TRUE enables.
+ * 'consent' => false, // Setting to FALSE disables.
+ * 'core' => null, // Unset or NULL uses default.
+ * ],
+ */
+
+ 'module.enable' => [
+ 'exampleauth' => true,
+ 'core' => true,
+ 'admin' => true,
+ 'saml' => true,
+ 'cron' => true,
+ 'metarefresh' => true,
+ ],
+
+
+ /*************************
+ | SESSION CONFIGURATION |
+ *************************/
+
+ /*
+ * This value is the duration of the session in seconds. Make sure that the time duration of
+ * cookies both at the SP and the IdP exceeds this duration.
+ */
+ 'session.duration' => 60, // 60 seconds
+
+ /*
+ * Sets the duration, in seconds, data should be stored in the datastore. As the data store is used for
+ * login and logout requests, this option will control the maximum time these operations can take.
+ * The default is 4 hours (4*60*60) seconds, which should be more than enough for these operations.
+ */
+ 'session.datastore.timeout' => (4 * 60 * 60), // 4 hours
+
+ /*
+ * Sets the duration, in seconds, auth state should be stored.
+ */
+ 'session.state.timeout' => (60 * 60), // 1 hour
+
+ /*
+ * Option to override the default settings for the session cookie name
+ */
+ 'session.cookie.name' => 'SimpleSAMLSessionIDidp',
+
+ /*
+ * Expiration time for the session cookie, in seconds.
+ *
+ * Defaults to 0, which means that the cookie expires when the browser is closed.
+ *
+ * Example:
+ * 'session.cookie.lifetime' => 30*60,
+ */
+ 'session.cookie.lifetime' => 0,
+
+ /*
+ * Limit the path of the cookies.
+ *
+ * Can be used to limit the path of the cookies to a specific subdirectory.
+ *
+ * Example:
+ * 'session.cookie.path' => '/simplesaml/',
+ */
+ 'session.cookie.path' => '/',
+
+ /*
+ * Cookie domain.
+ *
+ * Can be used to make the session cookie available to several domains.
+ *
+ * Example:
+ * 'session.cookie.domain' => '.example.org',
+ */
+ 'session.cookie.domain' => '',
+
+ /*
+ * Set the secure flag in the cookie.
+ *
+ * Set this to TRUE if the user only accesses your service
+ * through https. If the user can access the service through
+ * both http and https, this must be set to FALSE.
+ *
+ * If unset, SimpleSAMLphp will try to automatically determine the right value
+ */
+ //'session.cookie.secure' => true,
+
+ /*
+ * Set the SameSite attribute in the cookie.
+ *
+ * You can set this to the strings 'None', 'Lax', or 'Strict' to support
+ * the RFC6265bis SameSite cookie attribute. If set to null, no SameSite
+ * attribute will be sent.
+ *
+ * A value of "None" is required to properly support cross-domain POST
+ * requests which are used by different SAML bindings. Because some older
+ * browsers do not support this value, the canSetSameSiteNone function
+ * can be called to only set it for compatible browsers.
+ *
+ * You must also set the 'session.cookie.secure' value above to true.
+ *
+ * Example:
+ * 'session.cookie.samesite' => 'None',
+ */
+ 'session.cookie.samesite' => $httpUtils->canSetSameSiteNone() ? 'None' : null,
+
+ /*
+ * Options to override the default settings for php sessions.
+ */
+ 'session.phpsession.cookiename' => 'SimpleSAMLidp',
+ 'session.phpsession.savepath' => null,
+ 'session.phpsession.httponly' => true,
+
+ /*
+ * Option to override the default settings for the auth token cookie
+ */
+ 'session.authtoken.cookiename' => 'SimpleSAMLAuthToken',
+
+ /*
+ * Options for remember me feature for IdP sessions. Remember me feature
+ * has to be also implemented in authentication source used.
+ *
+ * Option 'session.cookie.lifetime' should be set to zero (0), i.e. cookie
+ * expires on browser session if remember me is not checked.
+ *
+ * Session duration ('session.duration' option) should be set according to
+ * 'session.rememberme.lifetime' option.
+ *
+ * It's advised to use remember me feature with session checking function
+ * defined with 'session.check_function' option.
+ */
+ 'session.rememberme.enable' => false,
+ 'session.rememberme.checked' => false,
+ 'session.rememberme.lifetime' => (14 * 86400),
+
+ /*
+ * Custom function for session checking called on session init and loading.
+ * See docs/simplesamlphp-advancedfeatures.md for function code example.
+ *
+ * Example:
+ * 'session.check_function' => ['\SimpleSAML\Module\example\Util', 'checkSession'],
+ */
+
+
+
+ /**************************
+ | MEMCACHE CONFIGURATION |
+ **************************/
+
+ /*
+ * Configuration for the 'memcache' session store. This allows you to store
+ * multiple redundant copies of sessions on different memcache servers.
+ *
+ * 'memcache_store.servers' is an array of server groups. Every data
+ * item will be mirrored in every server group.
+ *
+ * Each server group is an array of servers. The data items will be
+ * load-balanced between all servers in each server group.
+ *
+ * Each server is an array of parameters for the server. The following
+ * options are available:
+ * - 'hostname': This is the hostname or ip address where the
+ * memcache server runs. This is the only required option.
+ * - 'port': This is the port number of the memcache server. If this
+ * option isn't set, then we will use the 'memcache.default_port'
+ * ini setting. This is 11211 by default.
+ *
+ * When using the "memcache" extension, the following options are also
+ * supported:
+ * - 'weight': This sets the weight of this server in this server
+ * group. http://php.net/manual/en/function.Memcache-addServer.php
+ * contains more information about the weight option.
+ * - 'timeout': The timeout for this server. By default, the timeout
+ * is 3 seconds.
+ *
+ * Example of redundant configuration with load balancing:
+ * This configuration makes it possible to lose both servers in the
+ * a-group or both servers in the b-group without losing any sessions.
+ * Note that sessions will be lost if one server is lost from both the
+ * a-group and the b-group.
+ *
+ * 'memcache_store.servers' => [
+ * [
+ * ['hostname' => 'mc_a1'],
+ * ['hostname' => 'mc_a2'],
+ * ],
+ * [
+ * ['hostname' => 'mc_b1'],
+ * ['hostname' => 'mc_b2'],
+ * ],
+ * ],
+ *
+ * Example of simple configuration with only one memcache server,
+ * running on the same computer as the web server:
+ * Note that all sessions will be lost if the memcache server crashes.
+ *
+ * 'memcache_store.servers' => [
+ * [
+ * ['hostname' => 'localhost'],
+ * ],
+ * ],
+ *
+ * Additionally, when using the "memcached" extension, unique keys must
+ * be provided for each group of servers if persistent connections are
+ * desired. Each server group can also have an "options" indexed array
+ * with the options desired for the given group:
+ *
+ * 'memcache_store.servers' => [
+ * 'memcache_group_1' => [
+ * 'options' => [
+ * \Memcached::OPT_BINARY_PROTOCOL => true,
+ * \Memcached::OPT_NO_BLOCK => true,
+ * \Memcached::OPT_TCP_NODELAY => true,
+ * \Memcached::OPT_LIBKETAMA_COMPATIBLE => true,
+ * ],
+ * ['hostname' => '127.0.0.1', 'port' => 11211],
+ * ['hostname' => '127.0.0.2', 'port' => 11211],
+ * ],
+ *
+ * 'memcache_group_2' => [
+ * 'options' => [
+ * \Memcached::OPT_BINARY_PROTOCOL => true,
+ * \Memcached::OPT_NO_BLOCK => true,
+ * \Memcached::OPT_TCP_NODELAY => true,
+ * \Memcached::OPT_LIBKETAMA_COMPATIBLE => true,
+ * ],
+ * ['hostname' => '127.0.0.3', 'port' => 11211],
+ * ['hostname' => '127.0.0.4', 'port' => 11211],
+ * ],
+ * ],
+ *
+ */
+ 'memcache_store.servers' => [
+ [
+ ['hostname' => 'localhost'],
+ ],
+ ],
+
+ /*
+ * This value allows you to set a prefix for memcache-keys. The default
+ * for this value is 'simpleSAMLphp', which is fine in most cases.
+ *
+ * When running multiple instances of SSP on the same host, and more
+ * than one instance is using memcache, you probably want to assign
+ * a unique value per instance to this setting to avoid data collision.
+ */
+ 'memcache_store.prefix' => '',
+
+ /*
+ * This value is the duration data should be stored in memcache. Data
+ * will be dropped from the memcache servers when this time expires.
+ * The time will be reset every time the data is written to the
+ * memcache servers.
+ *
+ * This value should always be larger than the 'session.duration'
+ * option. Not doing this may result in the session being deleted from
+ * the memcache servers while it is still in use.
+ *
+ * Set this value to 0 if you don't want data to expire.
+ *
+ * Note: The oldest data will always be deleted if the memcache server
+ * runs out of storage space.
+ */
+ 'memcache_store.expires' => 36 * (60 * 60), // 36 hours.
+
+
+
+ /*************************************
+ | LANGUAGE AND INTERNATIONALIZATION |
+ *************************************/
+
+ /*
+ * Languages available, RTL languages, and what language is the default.
+ */
+ 'language.available' => [
+ 'en', 'no', 'nn', 'se', 'da', 'de', 'sv', 'fi', 'es', 'ca', 'fr', 'it', 'nl', 'lb',
+ 'cs', 'sk', 'sl', 'lt', 'hr', 'hu', 'pl', 'pt', 'pt-br', 'tr', 'ja', 'zh', 'zh-tw',
+ 'ru', 'et', 'he', 'id', 'sr', 'lv', 'ro', 'eu', 'el', 'af', 'zu', 'xh', 'st',
+ ],
+ 'language.rtl' => ['ar', 'dv', 'fa', 'ur', 'he'],
+ 'language.default' => 'en',
+
+ /*
+ * Options to override the default settings for the language parameter
+ */
+ 'language.parameter.name' => 'language',
+ 'language.parameter.setcookie' => true,
+
+ /*
+ * Options to override the default settings for the language cookie
+ */
+ 'language.cookie.name' => 'language',
+ 'language.cookie.domain' => '',
+ 'language.cookie.path' => '/',
+ 'language.cookie.secure' => true,
+ 'language.cookie.httponly' => false,
+ 'language.cookie.lifetime' => (60 * 60 * 24 * 900),
+ 'language.cookie.samesite' => $httpUtils->canSetSameSiteNone() ? 'None' : null,
+
+ /**
+ * Custom getLanguage function called from SimpleSAML\Locale\Language::getLanguage().
+ * Function should return language code of one of the available languages or NULL.
+ * See SimpleSAML\Locale\Language::getLanguage() source code for more info.
+ *
+ * This option can be used to implement a custom function for determining
+ * the default language for the user.
+ *
+ * Example:
+ * 'language.get_language_function' => ['\SimpleSAML\Module\example\Template', 'getLanguage'],
+ */
+
+ /**************
+ | APPEARANCE |
+ **************/
+
+ /*
+ * Which theme directory should be used?
+ */
+ 'theme.use' => 'default',
+
+ /*
+ * Set this option to the text you would like to appear at the header of each page. Set to false if you don't want
+ * any text to appear in the header.
+ */
+ //'theme.header' => 'SimpleSAMLphp',
+
+ /**
+ * A template controller, if any.
+ *
+ * Used to intercept certain parts of the template handling, while keeping away unwanted/unexpected hooks. Set
+ * the 'theme.controller' configuration option to a class that implements the
+ * \SimpleSAML\XHTML\TemplateControllerInterface interface to use it.
+ */
+ //'theme.controller' => '',
+
+ /*
+ * Templating options
+ *
+ * By default, twig templates are not cached. To turn on template caching:
+ * Set 'template.cache' to an absolute path pointing to a directory that
+ * SimpleSAMLphp has read and write permissions to.
+ */
+ //'template.cache' => '',
+
+ /*
+ * Set the 'template.auto_reload' to true if you would like SimpleSAMLphp to
+ * recompile the templates (when using the template cache) if the templates
+ * change. If you don't want to check the source templates for every request,
+ * set it to false.
+ */
+ 'template.auto_reload' => false,
+
+ /*
+ * Set this option to true to indicate that your installation of SimpleSAMLphp
+ * is running in a production environment. This will affect the way resources
+ * are used, offering an optimized version when running in production, and an
+ * easy-to-debug one when not. Set it to false when you are testing or
+ * developing the software, in which case a banner will be displayed to remind
+ * users that they're dealing with a non-production instance.
+ *
+ * Defaults to true.
+ */
+ 'production' => true,
+
+ /*
+ * SimpleSAMLphp modules can host static resources which are served through PHP.
+ * The serving of the resources can be configured through these settings.
+ */
+ 'assets' => [
+ /*
+ * These settings adjust the caching headers that are sent
+ * when serving static resources.
+ */
+ 'caching' => [
+ /*
+ * Amount of seconds before the resource should be fetched again
+ */
+ 'max_age' => 86400,
+ /*
+ * Calculate a checksum of every file and send it to the browser
+ * This allows the browser to avoid downloading assets again in situations
+ * where the Last-Modified header cannot be trusted,
+ * for example in cluster setups
+ *
+ * Defaults false
+ */
+ 'etag' => false,
+ ],
+ ],
+
+ /**
+ * Set to a full URL if you want to redirect users that land on SimpleSAMLphp's
+ * front page to somewhere more useful. If left unset, a basic welcome message
+ * is shown.
+ */
+ //'frontpage.redirect' => 'https://example.com/',
+
+ /*********************
+ | DISCOVERY SERVICE |
+ *********************/
+
+ /*
+ * Whether the discovery service should allow the user to save his choice of IdP.
+ */
+ 'idpdisco.enableremember' => true,
+ 'idpdisco.rememberchecked' => true,
+
+ /*
+ * The disco service only accepts entities it knows.
+ */
+ 'idpdisco.validate' => true,
+
+ 'idpdisco.extDiscoveryStorage' => null,
+
+ /*
+ * IdP Discovery service look configuration.
+ * Whether to display a list of idp or to display a dropdown box. For many IdP' a dropdown box
+ * gives the best use experience.
+ *
+ * When using dropdown box a cookie is used to highlight the previously chosen IdP in the dropdown.
+ * This makes it easier for the user to choose the IdP
+ *
+ * Options: [links,dropdown]
+ */
+ 'idpdisco.layout' => 'dropdown',
+
+
+
+ /*************************************
+ | AUTHENTICATION PROCESSING FILTERS |
+ *************************************/
+
+ /*
+ * Authentication processing filters that will be executed for all IdPs
+ */
+ 'authproc.idp' => [
+ /* Enable the authproc filter below to add URN prefixes to all attributes
+ 10 => [
+ 'class' => 'core:AttributeMap', 'addurnprefix'
+ ],
+ */
+ /* Enable the authproc filter below to automatically generated eduPersonTargetedID.
+ 20 => 'core:TargetedID',
+ */
+
+ // Adopts language from attribute to use in UI
+ 30 => 'core:LanguageAdaptor',
+
+ /* When called without parameters, it will fallback to filter attributes 'the old way'
+ * by checking the 'attributes' parameter in metadata on IdP hosted and SP remote.
+ */
+ 50 => 'core:AttributeLimit',
+
+ /*
+ * Search attribute "distinguishedName" for pattern and replaces if found
+ */
+ /*
+ 60 => [
+ 'class' => 'core:AttributeAlter',
+ 'pattern' => '/OU=studerende/',
+ 'replacement' => 'Student',
+ 'subject' => 'distinguishedName',
+ '%replace',
+ ],
+ */
+
+ /*
+ * Consent module is enabled (with no permanent storage, using cookies).
+ */
+ /*
+ 90 => [
+ 'class' => 'consent:Consent',
+ 'store' => 'consent:Cookie',
+ 'focus' => 'yes',
+ 'checked' => true
+ ],
+ */
+ // If language is set in Consent module it will be added as an attribute.
+ 99 => 'core:LanguageAdaptor',
+ ],
+
+ /*
+ * Authentication processing filters that will be executed for all SPs
+ */
+ 'authproc.sp' => [
+ /*
+ 10 => [
+ 'class' => 'core:AttributeMap', 'removeurnprefix'
+ ],
+ */
+
+ /*
+ * Generate the 'group' attribute populated from other variables, including eduPersonAffiliation.
+ 60 => [
+ 'class' => 'core:GenerateGroups', 'eduPersonAffiliation'
+ ],
+ */
+ /*
+ * All users will be members of 'users' and 'members'
+ */
+ /*
+ 61 => [
+ 'class' => 'core:AttributeAdd', 'groups' => ['users', 'members']
+ ],
+ */
+
+ // Adopts language from attribute to use in UI
+ 90 => 'core:LanguageAdaptor',
+ ],
+
+
+
+ /**************************
+ | METADATA CONFIGURATION |
+ **************************/
+
+ /*
+ * This option allows you to specify a directory for your metadata outside of the standard metadata directory
+ * included in the standard distribution of the software.
+ */
+ 'metadatadir' => 'metadata',
+
+ /*
+ * This option configures the metadata sources. The metadata sources is given as an array with
+ * different metadata sources. When searching for metadata, SimpleSAMLphp will search through
+ * the array from start to end.
+ *
+ * Each element in the array is an associative array which configures the metadata source.
+ * The type of the metadata source is given by the 'type' element. For each type we have
+ * different configuration options.
+ *
+ * Flat file metadata handler:
+ * - 'type': This is always 'flatfile'.
+ * - 'directory': The directory we will load the metadata files from. The default value for
+ * this option is the value of the 'metadatadir' configuration option, or
+ * 'metadata/' if that option is unset.
+ *
+ * XML metadata handler:
+ * This metadata handler parses an XML file with either an EntityDescriptor element or an
+ * EntitiesDescriptor element. The XML file may be stored locally, or (for debugging) on a remote
+ * web server.
+ * The XML metadata handler defines the following options:
+ * - 'type': This is always 'xml'.
+ * - 'file': Path to the XML file with the metadata.
+ * - 'url': The URL to fetch metadata from. THIS IS ONLY FOR DEBUGGING - THERE IS NO CACHING OF THE RESPONSE.
+ *
+ * MDQ metadata handler:
+ * This metadata handler looks up for the metadata of an entity at the given MDQ server.
+ * The MDQ metadata handler defines the following options:
+ * - 'type': This is always 'mdq'.
+ * - 'server': Base URL of the MDQ server. Mandatory.
+ * - 'validateCertificate': The certificates file that may be used to sign the metadata. You don't need this
+ * option if you don't want to validate the signature on the metadata. Optional.
+ * - 'cachedir': Directory where metadata can be cached. Optional.
+ * - 'cachelength': Maximum time metadata can be cached, in seconds. Defaults to 24
+ * hours (86400 seconds). Optional.
+ *
+ * PDO metadata handler:
+ * This metadata handler looks up metadata of an entity stored in a database.
+ *
+ * Note: If you are using the PDO metadata handler, you must configure the database
+ * options in this configuration file.
+ *
+ * The PDO metadata handler defines the following options:
+ * - 'type': This is always 'pdo'.
+ *
+ * Examples:
+ *
+ * This example defines two flatfile sources. One is the default metadata directory, the other
+ * is a metadata directory with auto-generated metadata files.
+ *
+ * 'metadata.sources' => [
+ * ['type' => 'flatfile'],
+ * ['type' => 'flatfile', 'directory' => 'metadata-generated'],
+ * ],
+ *
+ * This example defines a flatfile source and an XML source.
+ * 'metadata.sources' => [
+ * ['type' => 'flatfile'],
+ * ['type' => 'xml', 'file' => 'idp.example.org-idpMeta.xml'],
+ * ],
+ *
+ * This example defines an mdq source.
+ * 'metadata.sources' => [
+ * [
+ * 'type' => 'mdq',
+ * 'server' => 'http://mdq.server.com:8080',
+ * 'validateCertificate' => [
+ * '/var/simplesamlphp/cert/metadata-key.new.crt',
+ * '/var/simplesamlphp/cert/metadata-key.old.crt'
+ * ],
+ * 'cachedir' => '/var/simplesamlphp/mdq-cache',
+ * 'cachelength' => 86400
+ * ]
+ * ],
+ *
+ * This example defines an pdo source.
+ * 'metadata.sources' => [
+ * ['type' => 'pdo']
+ * ],
+ *
+ * Default:
+ * 'metadata.sources' => [
+ * ['type' => 'flatfile']
+ * ],
+ */
+ 'metadata.sources' => [
+ ['type' => 'flatfile'],
+ # webwork sp metadata dir
+ ['type' => 'flatfile', 'directory' => 'metadata/metarefresh-webwork'],
+ ],
+
+ /*
+ * Should signing of generated metadata be enabled by default.
+ *
+ * Metadata signing can also be enabled for a individual SP or IdP by setting the
+ * same option in the metadata for the SP or IdP.
+ */
+ 'metadata.sign.enable' => false,
+
+ /*
+ * The default key & certificate which should be used to sign generated metadata. These
+ * are files stored in the cert dir.
+ * These values can be overridden by the options with the same names in the SP or
+ * IdP metadata.
+ *
+ * If these aren't specified here or in the metadata for the SP or IdP, then
+ * the 'certificate' and 'privatekey' option in the metadata will be used.
+ * if those aren't set, signing of metadata will fail.
+ */
+ 'metadata.sign.privatekey' => null,
+ 'metadata.sign.privatekey_pass' => null,
+ 'metadata.sign.certificate' => null,
+ 'metadata.sign.algorithm' => 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
+
+
+ /****************************
+ | DATA STORE CONFIGURATION |
+ ****************************/
+
+ /*
+ * Configure the data store for SimpleSAMLphp.
+ *
+ * - 'phpsession': Limited datastore, which uses the PHP session.
+ * - 'memcache': Key-value datastore, based on memcache.
+ * - 'sql': SQL datastore, using PDO.
+ * - 'redis': Key-value datastore, based on redis.
+ *
+ * The default datastore is 'phpsession'.
+ */
+ 'store.type' => 'phpsession',
+
+ /*
+ * The DSN the sql datastore should connect to.
+ *
+ * See http://www.php.net/manual/en/pdo.drivers.php for the various
+ * syntaxes.
+ */
+ 'store.sql.dsn' => 'sqlite:/path/to/sqlitedatabase.sq3',
+
+ /*
+ * The username and password to use when connecting to the database.
+ */
+ 'store.sql.username' => null,
+ 'store.sql.password' => null,
+
+ /*
+ * The prefix we should use on our tables.
+ */
+ 'store.sql.prefix' => 'SimpleSAMLphp',
+
+ /*
+ * The driver-options we should pass to the PDO-constructor.
+ */
+ 'store.sql.options' => [],
+
+ /*
+ * The hostname and port of the Redis datastore instance.
+ */
+ 'store.redis.host' => 'localhost',
+ 'store.redis.port' => 6379,
+
+ /*
+ * The credentials to use when connecting to Redis.
+ *
+ * If your Redis server is using the legacy password protection (config
+ * directive "requirepass" in redis.conf) then you should only provide
+ * a password.
+ *
+ * If your Redis server is using ACL's (which are recommended as of
+ * Redis 6+) then you should provide both a username and a password.
+ * See https://redis.io/docs/manual/security/acl/
+ */
+ 'store.redis.username' => '',
+ 'store.redis.password' => '',
+
+ /*
+ * Communicate with Redis over a secure connection instead of plain TCP.
+ *
+ * This setting affects both single host connections as
+ * well as Sentinel mode.
+ */
+ 'store.redis.tls' => false,
+
+ /*
+ * Verify the Redis server certificate.
+ */
+ 'store.redis.insecure' => false,
+
+ /*
+ * Files related to secure communication with Redis.
+ *
+ * Files are searched in the 'certdir' when using relative paths.
+ */
+ 'store.redis.ca_certificate' => null,
+ 'store.redis.certificate' => null,
+ 'store.redis.privatekey' => null,
+
+ /*
+ * The prefix we should use on our Redis datastore.
+ */
+ 'store.redis.prefix' => 'SimpleSAMLphp',
+
+ /*
+ * The master group to use for Redis Sentinel.
+ */
+ 'store.redis.mastergroup' => 'mymaster',
+
+ /*
+ * The Redis Sentinel hosts.
+ * Example:
+ * 'store.redis.sentinels' => [
+ * 'tcp://[yoursentinel1]:[port]',
+ * 'tcp://[yoursentinel2]:[port]',
+ * 'tcp://[yoursentinel3]:[port]
+ * ],
+ *
+ * Use 'tls' instead of 'tcp' in order to make use of the additional
+ * TLS settings.
+ */
+ 'store.redis.sentinels' => [],
+
+ /*********************
+ | IdP/SP PROXY MODE |
+ *********************/
+
+ /*
+ * If the IdP in front of SimpleSAMLphp in IdP/SP proxy mode sends
+ * AuthnContextClassRef, decide whether the AuthnContextClassRef will be
+ * processed by the IdP/SP proxy or if it will be passed to the SP behind
+ * the IdP/SP proxy.
+ */
+ 'proxymode.passAuthnContextClassRef' => false,
+];
diff --git a/docker-config/idp/config/module_cron.php b/docker-config/idp/config/module_cron.php
new file mode 100644
index 0000000000..63a66ba3ed
--- /dev/null
+++ b/docker-config/idp/config/module_cron.php
@@ -0,0 +1,8 @@
+ 'healthcheck',
+ 'allowed_tags' => ['docker'],
+ 'debug_message' => true,
+ 'sendemail' => false,
+];
diff --git a/docker-config/idp/config/module_metarefresh.php b/docker-config/idp/config/module_metarefresh.php
new file mode 100644
index 0000000000..6c8975e524
--- /dev/null
+++ b/docker-config/idp/config/module_metarefresh.php
@@ -0,0 +1,19 @@
+ [
+ 'webwork' => [
+ 'cron' => ['docker'],
+ 'sources' => [
+ ['src' => $_ENV['SP_METADATA_URL']]
+ ],
+ 'expiresAfter' => 60*60*24*365*10, // 10 years, basically never
+ 'outputDir' => 'metadata/metarefresh-webwork/',
+ 'outputFormat' => 'flatfile',
+ ]
+ ]
+];
diff --git a/docker-config/idp/metadata/saml20-idp-hosted.php b/docker-config/idp/metadata/saml20-idp-hosted.php
new file mode 100644
index 0000000000..f0843f3b28
--- /dev/null
+++ b/docker-config/idp/metadata/saml20-idp-hosted.php
@@ -0,0 +1,50 @@
+ '__DEFAULT__',
+
+ // X.509 key and certificate. Relative to the cert directory.
+ 'privatekey' => 'server.pem',
+ 'certificate' => 'server.crt',
+
+ /*
+ * Authentication source to use. Must be one that is configured in
+ * 'config/authsources.php'.
+ */
+ 'auth' => 'example-userpass',
+
+ /* Uncomment the following to use the uri NameFormat on attributes. */
+ 'attributes.NameFormat' => 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri',
+ 'authproc' => [
+ // Convert attribute names to oids.
+ 100 => ['class' => 'core:AttributeMap', 'name2oid'],
+ ],
+
+ /*
+ * Uncomment the following to specify the registration information in the
+ * exported metadata. Refer to:
+ * http://docs.oasis-open.org/security/saml/Post2.0/saml-metadata-rpi/v1.0/cs01/saml-metadata-rpi-v1.0-cs01.html
+ * for more information.
+ */
+ /*
+ 'RegistrationInfo' => [
+ 'authority' => 'urn:mace:example.org',
+ 'instant' => '2008-01-17T11:28:03Z',
+ 'policies' => [
+ 'en' => 'http://example.org/policy',
+ 'es' => 'http://example.org/politica',
+ ],
+ ],
+ */
+];
diff --git a/lib/Mojolicious/Plugin/Saml2/Controller/AcsPostController.pm b/lib/Mojolicious/Plugin/Saml2/Controller/AcsPostController.pm
new file mode 100644
index 0000000000..1f1553fb04
--- /dev/null
+++ b/lib/Mojolicious/Plugin/Saml2/Controller/AcsPostController.pm
@@ -0,0 +1,101 @@
+package Mojolicious::Plugin::Saml2::Controller::AcsPostController;
+
+use Mojo::Base 'WeBWorK::Controller', -signatures, -async_await;
+
+use Mojo::JSON qw(decode_json);
+use Net::SAML2::Binding::POST;
+use Net::SAML2::Protocol::Assertion;
+
+use WeBWorK::Authen::Saml2;
+use WeBWorK::CourseEnvironment;
+use WeBWorK::DB;
+use WeBWorK::Debug qw(debug);
+
+async sub post ($c) {
+ debug('SAML2 is on!');
+ # check required params
+ my $samlResp = $c->param('SAMLResponse');
+ if (!$samlResp) {
+ return $c->reply->exception('Unauthorized - Missing SAMLResponse')->rendered(401);
+ }
+ my $relayState = $c->param('RelayState');
+ if (!$relayState) {
+ return $c->reply->exception('Unauthorized - Missing RelayState')->rendered(401);
+ }
+ $relayState = decode_json($relayState);
+
+ my $idp = $c->saml2->getIdp();
+ my $conf = $c->saml2->getConf();
+
+ # verify response is signed by the IdP and decode it
+ my $postBinding = Net::SAML2::Binding::POST->new(cacert => $c->saml2->getIdpCertFile());
+ my $decodedXml = $postBinding->handle_response($samlResp);
+ my $assertion = Net::SAML2::Protocol::Assertion->new_from_xml(
+ xml => $decodedXml,
+ key_file => $c->saml2->getSpSigningKeyFile()
+ );
+
+ $c->_actAsWebworkController($relayState->{course});
+ # get the authReqId we generated when we sent the user to the IdP
+ my $authReqId = $c->session->{authReqId};
+ delete $c->session->{authReqId}; # delete from session to avoid replay
+
+ # verify the response has the same authReqId which means it's responding to
+ # the auth request we generated, also checks that timestamps are valid
+ my $valid = $assertion->valid($conf->{sp}{entity_id}, $authReqId);
+ if (!$valid) {
+ return $c->reply->exception('Unauthorized - Bad timestamp or issuer')->rendered(401);
+ }
+
+ debug('Got valid response and looking for username');
+ my $userId = $c->_getUserId($conf->{sp}{attributes}, $assertion, $relayState);
+ if ($userId) {
+ debug("Got username $userId");
+ $c->authen->setSaml2UserId($userId);
+ if (!$c->authen->verify()) {
+ debug("Saml2 User Verify Failed");
+ debug("Rendering WeBWorK::ContentGenerator::Login");
+ return await WeBWorK::ContentGenerator::Login->new($c)->go();
+ }
+ return $c->redirect_to($relayState->{url});
+ }
+ return $c->reply->exception('Unauthorized - User not found in ' . $relayState->{course})->rendered(401);
+}
+
+sub _actAsWebworkController ($c, $courseName) {
+ # we need to call Webwork authen module to create the auth session, so our
+ # controller need to have the things that the authen module needs to use
+ $c->stash('courseID', $courseName);
+ $c->ce(WeBWorK::CourseEnvironment->new({ courseName => $courseName }));
+ $c->db(WeBWorK::DB->new($c->ce->{dbLayout}));
+ my $authz = WeBWorK::Authz->new($c);
+ $c->authz($authz);
+ my $authen = WeBWorK::Authen::Saml2->new($c);
+ $c->authen($authen);
+}
+
+sub _getUserId ($c, $attributeKeys, $assertion, $relayState) {
+ my $ce = $c->{ce};
+ my $db = $c->{db};
+ my $user;
+ if ($attributeKeys) {
+ foreach my $key (@$attributeKeys) {
+ debug("Trying attribute $key for username");
+ my $possibleUserId = $assertion->attributes->{$key}->[0];
+ if (!$possibleUserId) { next; }
+ if ($db->getUser($possibleUserId)) {
+ debug("Using attribute value for username: $possibleUserId");
+ return $possibleUserId;
+ }
+ }
+ }
+ debug("No username match in attributes, trying NameID fallback");
+ if ($db->getUser($assertion->nameid)) {
+ debug("Using NameID for username: " . $assertion->nameid);
+ return $assertion->nameid;
+ }
+ debug("NameID fallback failed, no username possible");
+ return '';
+}
+
+1;
diff --git a/lib/Mojolicious/Plugin/Saml2/Controller/ErrorController.pm b/lib/Mojolicious/Plugin/Saml2/Controller/ErrorController.pm
new file mode 100644
index 0000000000..1903a2c49e
--- /dev/null
+++ b/lib/Mojolicious/Plugin/Saml2/Controller/ErrorController.pm
@@ -0,0 +1,9 @@
+package Mojolicious::Plugin::Saml2::Controller::ErrorController;
+
+use Mojo::Base 'Mojolicious::Controller', -signatures, -async_await;
+
+async sub get ($c) {
+ return $c->reply->exception('SAML2 Login Error')->rendered(400);
+}
+
+1;
diff --git a/lib/Mojolicious/Plugin/Saml2/Controller/MetadataController.pm b/lib/Mojolicious/Plugin/Saml2/Controller/MetadataController.pm
new file mode 100644
index 0000000000..bd74129093
--- /dev/null
+++ b/lib/Mojolicious/Plugin/Saml2/Controller/MetadataController.pm
@@ -0,0 +1,10 @@
+package Mojolicious::Plugin::Saml2::Controller::MetadataController;
+
+use Mojo::Base 'Mojolicious::Controller', -signatures, -async_await;
+
+async sub get ($c) {
+ my $sp = $c->saml2->getSp();
+ return $c->render(data => $sp->metadata(), format => 'xml');
+}
+
+1;
diff --git a/lib/Mojolicious/Plugin/Saml2/Exception.pm b/lib/Mojolicious/Plugin/Saml2/Exception.pm
new file mode 100644
index 0000000000..43d5cd88e0
--- /dev/null
+++ b/lib/Mojolicious/Plugin/Saml2/Exception.pm
@@ -0,0 +1,3 @@
+package Mojolicious::Plugin::Saml2::Exception;
+use Mojo::Base 'Mojo::Exception', -signatures;
+1;
diff --git a/lib/Mojolicious/Plugin/Saml2/README.md b/lib/Mojolicious/Plugin/Saml2/README.md
new file mode 100644
index 0000000000..f531c80ea9
--- /dev/null
+++ b/lib/Mojolicious/Plugin/Saml2/README.md
@@ -0,0 +1,163 @@
+# SAML2 Authentication Plugin
+
+This Mojolicious plugin implements SAML2 authentication for Webwork. SAML2
+functionality is provided by the
+[Net::SAML2](https://metacpan.org/dist/Net-SAML2) library. Net::SAML2 claims to
+be compatible with a wide array of SAML2 based Single Sign On systems such as
+Shibboleth. This plugin is intended to replace the previous Shibboleth
+authentication module that depended on Apache mod_shib.
+
+There are two components to SAML2 support, the Mojolicious plugin here and a a
+regular Webwork Authen module at `lib/WeBWorK/Authen/Saml2.pm`.
+
+## Configuration
+
+To enable the Saml2 plugin, copy `conf/authen_saml2.dist.yml` to
+`conf/authen_saml2.yml`.
+
+Important settings:
+
+- *idp.metadata_url* - must be set to the IdP's metadata endpoint
+- *sp.entity_id* - the ID for the Webwork SP, this is usually the application
+ root URL plus the base path to the SP
+- *sp.attributes* - list of attribute OIDs that the SP will look at and try to
+ match to a Webwork username
+- *sp.cert*, *sp.signing_key* - a unique key and cert pair must be generated
+ for your own prod deployments. The example key and cert is only meant for dev
+ use as described below in [Docker Compose](#docker-compose-dev).
+
+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 with the
+default config. Endpoint locations, such as metadata, can be configured under
+`sp.route`.
+
+### Generate key and cert
+
+OpenSSL can be used to generate the key and cert, like the following command:
+
+```bash
+openssl req -newkey rsa:4096 -new -x509 -days 3652 -nodes -out saml.crt -keyout saml.pem
+```
+
+The cert is placed in `saml.crt`. The key is in `saml.pem`.
+
+### localOverrides.conf
+
+Webwork's authentication system will need to be configured to use the Saml2
+module in `conf/localOverrides.conf`. The example below allows bypassing the
+Saml2 module to use the internal username/password login as a fallback:
+
+```perl
+$authen{user_module} = [
+ 'WeBWorK::Authen::Saml2',
+ 'WeBWorK::Authen::Basic_TheLastOption'
+];
+```
+
+If you add the bypass query to a course url, the Saml2 module will be skipped
+and the next one in the list used, e.g.:
+`http://localhost:8080/webwork2/TEST100?bypassSaml2=1`
+
+Admin login also needs its own config, the example below assumes the bypass
+option is disabled:
+
+```perl
+$authen{admin_module} = [
+ 'WeBWorK::Authen::Saml2'
+];
+```
+
+To disable the bypass, `conf/authen_saml2.yml` must also be edited, commenting
+out the `bypass_query` line.
+
+## Docker Compose Dev
+
+A dev use SAML2 IdP was added to docker-compose.yml.dist, to start this IdP
+along with the rest of the Webwork, add the '--profile saml2dev' arg to docker
+compose:
+
+```bash
+docker compose --profile saml2dev up
+```
+
+Without the profile arg, the IdP services do not start. The dev IdP is a
+SimpleSAMLphp instance.
+
+### Setup
+
+The default `conf/authen_saml2.dist.yml` is configured to use this dev IdP.
+Just copy it to `conf/authen_saml2.yml` and it should work.
+
+### Admin
+
+The dev IdP has an admin interface, you can login with the password 'admin' at:
+
+```text
+http://localhost:8180/simplesaml/module.php/admin/federation
+```
+
+The admin interface lets you check if the IdP has properly registered the
+Webwork SP under the 'Federation' tab, it should be listed under the "Trusted
+entities" section.
+
+You can also test login with the user accounts listed below in the "Test" tab
+under the "example-userpass" authentication source.
+
+### Users
+
+There are some single sign-on accounts preconfigured:
+
+- Username: student01
+ - Password: student01
+- Username: instructor01
+ - Password: instructor01
+- Username: staff01
+ - Password: staff01
+
+You can add more accounts at `docker-config/idp/config/authsources.php` in the
+`example-userpass` section. The IdP image will need to be rebuilt for the
+change to take effect.
+
+## Troubleshooting
+
+### Webwork doesn't start, "Error retrieving metadata"
+
+This error message indicates that the Saml2 plugin wasn't able to grab metadata
+from the IdP metadata url. Make sure the IdP is accessible by the container.
+Example error message:
+
+```text
+app-1 | Can't load application from file "/opt/webwork/webwork2/bin/webwork2":
+Error retrieving metadata: Can't connect to idp.docker:8180 (Connection
+refused) (500)
+```
+
+### User not found in course
+
+The user was verified by the IdP but did not have a corresponding user account
+in the Webwork course. The Webwork user account needs to be created separately
+as the Saml2 plugin does not do user provisioning.
+
+### Logout shows uninitialized value warnings
+
+The message on the page reads "The course TEST100 uses an external
+authentication system ()."
+
+The external auth message takes values from LTI config. If you're not using
+LTI, you can define the missing values separately in `localOverrides.conf`:
+
+```perl
+$LTIVersion = 'v1p3';
+$LTI{v1p3}{LMS_name} = 'Webwork';
+$LTI{v1p3}{LMS_url} = 'http://localhost:8080/';
+```
+
+It's not an ideal solution but the Saml2 plugin needs to declare itself as an
+external auth system in order to avoid the internal 2FA. And the external auth
+message assumes LTI is on.
+
+### Dev IdP does not show the Webwork SP in Federation tab
+
+Webwork's first startup might be slow enough that the IdP wasn't able to
+successfully grab metadata from the Webwork Saml2 plugin. Restarting everything
+should fix this.
diff --git a/lib/Mojolicious/Plugin/Saml2/Router.pm b/lib/Mojolicious/Plugin/Saml2/Router.pm
new file mode 100644
index 0000000000..ac4dd5cb7b
--- /dev/null
+++ b/lib/Mojolicious/Plugin/Saml2/Router.pm
@@ -0,0 +1,17 @@
+package Mojolicious::Plugin::Saml2::Router;
+
+use Mojo::Base -signatures;
+
+sub setup ($app, $conf) {
+ my $subRouter =
+ $app->routes->any($conf->{sp}{route}{base})->to(namespace => 'Mojolicious::Plugin::Saml2::Controller')
+ ->name('saml2.base');
+ $subRouter->get($conf->{sp}{route}{metadata})->to(controller => 'MetadataController', action => 'get')
+ ->name('saml2.metadata');
+ $subRouter->get($conf->{sp}{route}{error})->to(controller => 'ErrorController', action => 'get')
+ ->name('saml2.error');
+ $subRouter->post($conf->{sp}{route}{acs}{post})->to(controller => 'AcsPostController', action => 'post')
+ ->name('saml2.acsPost');
+}
+
+1;
diff --git a/lib/Mojolicious/Plugin/Saml2/Saml2Plugin.pm b/lib/Mojolicious/Plugin/Saml2/Saml2Plugin.pm
new file mode 100644
index 0000000000..4073520e1c
--- /dev/null
+++ b/lib/Mojolicious/Plugin/Saml2/Saml2Plugin.pm
@@ -0,0 +1,155 @@
+package Mojolicious::Plugin::Saml2::Saml2Plugin;
+use Mojo::Base 'Mojolicious::Plugin', -signatures;
+# external libs
+use File::Temp qw/ tempfile /;
+use Mojo::JSON qw(encode_json);
+use Mojolicious;
+use Mojolicious::Plugin::NotYAMLConfig;
+use Net::SAML2::IdP;
+use Net::SAML2::SP;
+use URN::OASIS::SAML2 qw(BINDING_HTTP_POST BINDING_HTTP_REDIRECT);
+# external libs for NotYAMLConfig
+use CPAN::Meta::YAML;
+use Mojo::Util qw(decode encode);
+# webwork modules
+use WeBWorK::Debug qw(debug);
+# plugin's own modules
+use Mojolicious::Plugin::Saml2::Exception;
+use Mojolicious::Plugin::Saml2::Router;
+
+use constant Exception => 'Mojolicious::Plugin::Saml2::Exception';
+
+our $VERSION = '0.0.1';
+
+sub register ($self, $app, $conf = {}) {
+ # yml config can be overridden with config passed in during plugin init
+ $conf = $self->_loadConf($conf, $app);
+ $self->checkConf($conf);
+ # note this will grab the IdP metadata on every server reboot
+ my $idp = Net::SAML2::IdP->new_from_url(url => $conf->{idp}{metadata_url});
+ my $spCertFile = $self->_getTmpFileWithContent($conf->{sp}{cert});
+ my $spSigningKeyFile = $self->_getTmpFileWithContent($conf->{sp}{signing_key});
+ my $idpCertFile = $self->_getTmpFileWithContent($idp->cert('signing')->[0]);
+ # setup routes for metadata and samlresponse handling
+ Mojolicious::Plugin::Saml2::Router::setup($app, $conf);
+ # cached values we need later
+ $app->helper('saml2.getConf' => sub { return $conf; });
+ $app->helper('saml2.getIdp' => sub { return $idp; });
+ $app->helper('saml2.getSpCertFile' => sub { return $spCertFile; });
+ $app->helper('saml2.getSpSigningKeyFile' => sub { return $spSigningKeyFile; });
+ $app->helper('saml2.getIdpCertFile' => sub { return $idpCertFile; });
+ $app->helper('saml2.getSp' => \&getSp);
+ # called by the Webwork Saml2 authen module to redirect users to the IdP
+ $app->helper('saml2.sendLoginRequest' => \&sendLoginRequest);
+}
+
+sub checkConf ($self, $conf) {
+ if (!$conf->{idp}) {
+ Exception->throw("Config missing 'idp' section");
+ }
+ if (!$conf->{idp}{metadata_url}) {
+ Exception->throw("Config in 'idp' missing 'metadata_url'");
+ }
+ if (!$conf->{sp}) {
+ Exception->throw("Config missing 'sp' section");
+ }
+ if (!$conf->{sp}{entity_id}) {
+ Exception->throw("Config in 'sp' missing 'entity_id'");
+ }
+ if (!$conf->{sp}{cert}) {
+ Exception->throw("Config in 'sp' missing 'cert'");
+ }
+ if (!$conf->{sp}{signing_key}) {
+ Exception->throw("Config in 'sp' missing 'signing_key'");
+ }
+ if (!$conf->{sp}{route}) {
+ Exception->throw("Config missing 'sp.route' section");
+ }
+ if (!$conf->{sp}{route}{base}) {
+ Exception->throw("Config in 'sp.route' missing 'base'");
+ }
+ if (!$conf->{sp}{route}{metadata}) {
+ Exception->throw("Config in 'sp.route' missing 'metadata'");
+ }
+ if (!$conf->{sp}{route}{acs}) {
+ Exception->throw("Config missing 'sp.route.acs' section");
+ }
+ if (!$conf->{sp}{route}{acs}{post}) {
+ Exception->throw("Config in 'sp.route.acs' missing 'post'");
+ }
+}
+
+# we need an SP instance in order to generate the xml metadata and specify our
+# SP endpoints. We have to do this in a helper cause we need to use the
+# controller's url_for()
+sub getSp ($c) {
+ state $sp;
+ if ($sp) { return $sp; }
+ my $conf = $c->saml2->getConf();
+ $sp = Net::SAML2::SP->new(
+ issuer => $conf->{sp}->{entity_id},
+ # base url for SP services
+ url => $ENV{WEBWORK_ROOT_URL} . $c->url_for('saml2.base'),
+ error_url => $ENV{WEBWORK_ROOT_URL} . $c->url_for('saml2.error'),
+ cert => $c->saml2->getSpCertFile(),
+ key => $c->saml2->getSpSigningKeyFile(),
+ org_contact => $conf->{sp}->{org}->{contact},
+ org_name => $conf->{sp}->{org}->{name},
+ org_url => $conf->{sp}->{org}->{url},
+ org_display_name => $conf->{sp}->{org}->{display_name},
+ assertion_consumer_service => [ {
+ Binding => BINDING_HTTP_POST,
+ Location => $ENV{WEBWORK_ROOT_URL} . $c->url_for('saml2.acsPost'),
+ isDefault => 'true',
+ } ]
+ );
+ return $sp;
+}
+
+# $returnUrl is the course URL that the user should be directed into after they
+# sucessfully authed at the IdP
+sub sendLoginRequest ($c, $returnUrl, $courseName) {
+ debug('Creating Login Request');
+ my $conf = $c->saml2->getConf();
+ my $idp = $c->saml2->getIdp();
+ my $sp = $c->saml2->getSp();
+ my $authReq = $sp->authn_request($idp->sso_url(BINDING_HTTP_REDIRECT));
+ $c->session->{authReqId} = $authReq->id;
+ my $redirect = $sp->sso_redirect_binding($idp, 'SAMLRequest');
+ # info the IdP relays back to help us put the user in the right place after
+ # login
+ my $relayState = {
+ 'course' => $courseName,
+ 'url' => $returnUrl
+ };
+ my $url = $redirect->sign($authReq->as_xml, encode_json($relayState));
+ debug('Redirecting user to the IdP');
+ $c->redirect_to($url);
+}
+
+# Write $content into a temporary file and return the full path to that file.
+# Net:SAML2 strangely won't take keys and certs as strings, it only wants
+# filepaths, this helper is meant to get around that.
+sub _getTmpFileWithContent ($self, $content) {
+ my ($fh, $filename) = tempfile();
+ print $fh $content;
+ close($fh);
+ return $filename;
+}
+
+sub _loadConf ($self, $pluginConf, $app) {
+ my $confFile = "$ENV{WEBWORK_ROOT}/conf/authen_saml2.yml";
+ if (!-e $confFile) {
+ Exception->throw("Missing conf file: $confFile");
+ }
+ $app->config->{config_override} = 1;
+ my $yamlPlugin = Mojolicious::Plugin::NotYAMLConfig->new;
+ # we just want to use the plugin's load() method and don't want to merge
+ # with the app config, so we have to manually do the setup done in
+ # NotYAMLConfig's register()
+ $yamlPlugin->{yaml} = sub { CPAN::Meta::YAML::Load(decode 'UTF-8', shift) };
+ my $yamlConf = $yamlPlugin->load($confFile, {}, $app);
+ return { %$yamlConf, %$pluginConf };
+}
+
+1;
diff --git a/lib/Mojolicious/WeBWorK.pm b/lib/Mojolicious/WeBWorK.pm
index 66281467f9..ef3a1c8fc0 100644
--- a/lib/Mojolicious/WeBWorK.pm
+++ b/lib/Mojolicious/WeBWorK.pm
@@ -94,6 +94,10 @@ sub startup ($app) {
# Provide the ability to serve data as a file download.
$app->plugin('RenderFile');
+ # Load the SAML2 plugin if configuration found
+ if (-e "$ENV{WEBWORK_ROOT}/conf/authen_saml2.yml") {
+ $app->plugin('Mojolicious::Plugin::Saml2::Saml2Plugin');
+ }
# Helpers
diff --git a/lib/WeBWorK/Authen/Saml2.pm b/lib/WeBWorK/Authen/Saml2.pm
new file mode 100644
index 0000000000..ef02074cc5
--- /dev/null
+++ b/lib/WeBWorK/Authen/Saml2.pm
@@ -0,0 +1,102 @@
+################################################################################
+# WeBWorK Online Homework Delivery System
+# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of either: (a) the GNU General Public License as published by the
+# Free Software Foundation; either version 2, or (at your option) any later
+# version, or (b) the "Artistic License" which comes with this package.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the
+# Artistic License for more details.
+################################################################################
+
+package WeBWorK::Authen::Saml2;
+use Mojo::Base 'WeBWorK::Authen', -signatures;
+
+use WeBWorK::Debug qw(debug);
+
+=head1 NAME
+
+WeBWorK::Authen::Saml2 - Sends everyone to the SAML2 IdP to authenticate.
+
+Requires the Saml2 plugin to be loaded and configured.
+
+=cut
+
+sub request_has_data_for_this_verification_module ($self) {
+ my $c = $self->{c};
+ $self->setIsLoggedIn(0);
+
+ # skip if Saml2 plugin config is missing, this means the plugin isn't loaded
+ if (!-e "$ENV{WEBWORK_ROOT}/conf/authen_saml2.yml") {
+ debug('Saml2 Authen Module requires Saml2 plugin to be configured');
+ return 0;
+ }
+ # skip if we have the param that indicates we want to bypass SAML2
+ my $bypassQuery = $c->saml2->getConf->{bypass_query};
+ if ($bypassQuery && $c->param($bypassQuery)) {
+ debug('Saml2 Authen module bypass detected, going to next module');
+ return 0;
+ }
+ # handle as existing session if we have cookie or if it's a rpc
+ my ($cookieUser, $cookieKey, $cookieTimeStamp) = $self->fetchCookie;
+ if (defined $cookieUser || defined $c->{rpc}) {
+ $self->setIsLoggedIn(1);
+ }
+
+ return 1;
+}
+
+sub do_verify ($self) {
+ if ($self->{saml2UserId} || $self->{isLoggedIn}) {
+ # successful saml response/already logged in, hand off to the parent
+ # to create/read the session
+ $self->{external_auth} = 1; # so we skip internal 2fa
+ return $self->SUPER::do_verify();
+ }
+ # user doesn't have an existing session, send them to IdP for login
+ my $c = $self->{c};
+ my $ce = $c->{ce};
+ debug('User needs to go to the IdP for login');
+ debug('If login successful, user should be in course: ' . $ce->{courseName});
+ debug('With the URL ' . $c->req->url);
+ $c->saml2->sendLoginRequest($c->req->url->to_string, $ce->{courseName});
+
+ # we fail verify for this request but doesn't matter cause the user gets
+ # redirected to the IdP
+ return 0;
+}
+
+sub get_credentials ($self) {
+ if ($self->{saml2UserId}) {
+ # user has been authed by the IdP
+ $self->{user_id} = $self->{saml2UserId};
+ $self->{login_type} = "normal";
+ $self->{credential_source} = "SAML2";
+ $self->{session_key} = undef;
+ $self->{initial_login} = 1;
+ return 1;
+ }
+ if ($self->{isLoggedIn}) {
+ return $self->SUPER::get_credentials();
+ }
+ return 0;
+}
+
+sub authenticate ($self) {
+ # idp has authenticated us, so we can just return 1
+ return 1;
+}
+
+sub setSaml2UserId ($self, $userId) {
+ $self->{saml2UserId} = $userId;
+}
+
+sub setIsLoggedIn ($self, $val) {
+ $self->{isLoggedIn} = $val;
+}
+
+1;