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.conf.dist b/conf/authen_saml2.conf.dist
new file mode 100644
index 0000000000..a4403134a1
--- /dev/null
+++ b/conf/authen_saml2.conf.dist
@@ -0,0 +1,137 @@
+#!perl
+################################################################################
+# Configuration for using Saml2 authentication.
+# To enable Saml2 authentication, copy this file to conf/authen_saml2.conf
+# and uncomment the appropriate lines in localOverrides.conf. The Saml2
+# authentication module uses the Net::SAML2 library. The library claims to be
+# compatible with a wide range of SAML2 implementations, including Shibboleth.
+################################################################################
+
+# Set Saml2 as the authentication module to use.
+# Comment out 'WeBWorK::Authen::Basic_TheLastOption' if bypassing Saml2
+# authentication is not allowed (see $saml2{bypass_query} below).
+$authen{user_module} = [
+ 'WeBWorK::Authen::Saml2',
+ 'WeBWorK::Authen::Basic_TheLastOption'
+];
+
+# List of authentication modules that may be used to enter the admin course.
+# This is used instead of $authen{user_module} when logging into the admin
+# course. Since the admin course provides overall power to add/delete courses,
+# access to this course should be protected by the best possible authentication
+# you have available to you.
+$authen{admin_module} = [
+ 'WeBWorK::Authen::Saml2'
+];
+
+# This URL query parameter can be added to the end of a course url to skip the
+# saml2 authentication module and go to the next one, for example,
+# http://your.school.edu/webwork2/courseID?bypassSaml2=1. Comment out the next
+# line to disable this feature.
+$saml2{bypass_query} = 'bypassSaml2';
+
+# Note that Saml2 authentication can be used in conjunction with webwork's two
+# factor authentication. If the identity provider does not provide two factor
+# authentication, then it is recommended that you DO use webwork's two factor
+# authentication. If the identity provider does provide two factor
+# authentication, then you would not want your users need to perform two factor
+# authentication twice, so you should disable webwork's two factor
+# authentication. The two factor authentication settings are set in
+# localOverrides.conf.
+
+# As noted above, if the identity provider offers two factor authentication,
+# then you would not want webwork2's two factor authentication to be used at the
+# same time. However, if the bypass parameter is allowed, you should still
+# enable two factor authentication in that case. If this is the case, then set
+# $saml2{twoFAOnlyWithBypass} to 1. This will skip webwork2's two factor
+# authentication for users signing in via the identity provider, but still
+# require it for users signing in with a username/password. If this is set to 0,
+# then webwork2's two factor authentication will always be required.
+$saml2{twoFAOnlyWithBypass} = 0;
+
+# If $external_auth is 1, and the authentication sequence reaches
+# Basic_TheLastOption, then the webwork login screen will show a message
+# directing the user to use the external authentication system to login. This
+# prevents users from attempting to login in to WeBWorK directly.
+$external_auth = 0;
+
+# The $saml2{idps} hash contains names of identity proviers and their SAML2
+# metadata URLs that are used by this server. Webwork will request the identity
+# provider's metadata from the URL of the $saml2{active_idp} during the
+# authentication process. Additional identity providers can also be added for a
+# particular course by adding, for example, $saml2{idps}{other_idp} = '...' to
+# the course.conf file of the course. Note that the names of the identity
+# providers in this hash are used for a directory name in which the metadata and
+# certificate for the identity provider are saved. So the names should only
+# contain alpha numeric characters and underscores.
+$saml2{idps} = {
+ default => 'http://idp/simplesaml/module.php/saml/idp/metadata',
+ # Add additional identity providers used by this server below.
+ #other_idp => 'http://other.idp.server/metadata',
+};
+
+# The $saml2{active_idp} is the identity provider in the $saml2{idps} hash that
+# will be used. If different identity providers are used for different courses,
+# then set $saml2{active_idp} = 'other_idp' in the course.conf file of each
+# course.
+$saml2{active_idp} = 'default';
+
+# This the id for the webwork2 service provider. This is usually the application
+# root URL plus the base path to the service provider.
+$saml2{sp}{entity_id} = 'http://localhost:8080/webwork2/saml2';
+
+# This is the organization metadata information for the webwork2 service
+# provider. The Saml2 authentication module will generate xml metadata that can
+# be obtained by the identity provider for configuration from the URL
+# https://webwork.yourschool.edu/webwork2/saml2/metadata if Saml2 authentication
+# is enabled site wide. The URL needs to have the courseID URL parameter added
+# if Saml2 authentication is not enabled site wide, but is enabled for some
+# courses in those course's course.conf files. So for example if one course is
+# myTestCourse, then the metadata URL would be
+# https://webwork.yourschool.edu/webwork2/saml2/metadata?courseID=myTestCourse
+# Further note that if multiple courses use that same identity provider then
+# just pick any one of the courses to use in the metadata URL. All of the other
+# courses share the same metedata.
+$saml2{sp}{org} = {
+ contact => 'webwork@example.edu',
+ name => 'webwork',
+ url => 'https://localhost:8080/',
+ display_name => 'WeBWorK'
+};
+
+# The following list of attributes will be checked in the given order for a
+# matching user in the webwork2 course. If no attributes are given, then
+# webwork2 will default to the NameID. It is recommended that you use the
+# attribute's OID.
+$saml2{sp}{attributes} = [
+ 'urn:oid:0.9.2342.19200300.100.1.1'
+];
+
+# The following settings are the locations of the files that contain the
+# certificate and private key for the webwork2 service provider. A certificate
+# and private key can be generated using openssl. For example,
+# openssl req -newkey rsa:3072 -new -x509 -days 3652 -nodes -out saml.crt -keyout saml.pem
+# The files saml.crt and saml.pem that are generated contain the public
+# "certificate" and the "private_key", respectively.
+# Note that if the files are placed within the root webwork2 app directory, then
+# the paths may be given relative to the root webwork2 app directory. Otherwise
+# the absolute path must be given. Make sure that the webwork2 app has read
+# permissions for those files.
+$saml2{sp}{certificate_file} = 'docker-config/idp/certs/saml.crt';
+$saml2{sp}{private_key_file} = 'docker-config/idp/certs/saml.pem';
+
+##############################################################################
+# SECURITY WARNING
+# For production, you MUST provide your own unique 'certificate' and
+# 'private_key' files. The files referred to in the default settings above are
+# only intended to be used in development, and are publicly exposed. Hence, they
+# provide NO SECURITY.
+##############################################################################
+
+# If this is set to 1, then service provider initiated logout from the identity
+# provider is enabled. This means that when the user clicks the webwork2 "Log
+# Out" button, a request is sent to the identity provider that also ends the
+# session for the user with the identity provider.
+$saml2{sp}{enable_sp_initiated_logout} = 0;
+
+1;
diff --git a/conf/localOverrides.conf.dist b/conf/localOverrides.conf.dist
index 555f2f0f91..c852f924be 100644
--- a/conf/localOverrides.conf.dist
+++ b/conf/localOverrides.conf.dist
@@ -537,6 +537,15 @@ $mail{feedbackRecipients} = [
#include("conf/authen_shibboleth.conf");
+# Saml2 Authentication
+################################################################################
+# Uncomment the following line to enable authentication via a Saml2 identity
+# provider. You will also need to copy the file authen_saml2.conf.dist to
+# authen_saml2.conf, and then edit that file to fill in the settings for your
+# installation.
+
+#include("conf/authen_saml2.conf");
+
################################################################################
# Session Management
################################################################################
diff --git a/docker-config/docker-compose.dist.yml b/docker-config/docker-compose.dist.yml
index c5d51e06b9..b74d5276d7 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,29 @@ services:
#ports:
# - "6311:6311"
+ # SimpleSAMLphp Saml2 identity provider for development use only. This is a separate profile from the other services
+ # so it doesn't start in normal usage. Use "docker compose --profile saml2dev up" to start, "docker compose --profile
+ # saml2dev stop" to stop services, and "docker compose --profile saml2dev down" to stop services and remove
+ # containers.
+ idp:
+ build:
+ context: ./docker-config/idp/
+ profiles:
+ - saml2dev
+ ports:
+ - '8180:80'
+ environment:
+ SP_METADATA_URL: 'http://app:8080/webwork2/saml2/metadata'
+ # The healthcheck url is SimpleSAMLphp's url for triggering cron jobs. The cron job it triggers will
+ # automatically fetch the webwork2 service provider's metadata.
+ healthcheck:
+ test: ['CMD', 'curl', '-f', 'http://localhost/simplesaml/module.php/cron/run/metarefresh/webwork2']
+ start_period: 1m
+ start_interval: 15s
+ interval: 1h
+ retries: 1
+ timeout: 10s
+
volumes:
oplVolume:
driver: local
diff --git a/docker-config/idp/Dockerfile b/docker-config/idp/Dockerfile
new file mode 100644
index 0000000000..876ba9f7d0
--- /dev/null
+++ b/docker-config/idp/Dockerfile
@@ -0,0 +1,38 @@
+FROM php:8.3-apache
+WORKDIR /var/www
+
+# Install composer and the 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
+
+# Directories used by simplesamlphp. These need to be accessible by the apache2 user.
+RUN mkdir simplesamlphp/ /var/cache/simplesamlphp
+RUN chown www-data simplesamlphp/ /var/cache/simplesamlphp
+
+COPY ./idp.apache2.conf /etc/apache2/conf-available
+RUN a2enconf idp.apache2
+
+# Composer doesn't like to be root, so run the rest as the apache user.
+USER www-data
+
+# Install simplesamlphp
+RUN git clone --branch v2.2.1 https://github.com/simplesamlphp/simplesamlphp.git
+WORKDIR /var/www/simplesamlphp
+
+# Generate the server certificates.
+RUN cd cert/ && \
+ openssl req -newkey rsa:3072 -new -x509 -days 3652 -nodes -out server.crt -keyout server.pem \
+ -subj "/C=US/S=New York/L=Rochester/O=WeBWorK/CN=idp.webwork2"
+
+# Use composer to install dependencies.
+RUN composer install && \
+ composer require simplesamlphp/simplesamlphp-module-metarefresh
+
+# Copy configuration files.
+COPY ./config/ config/
+COPY ./metadata/ metadata/
+
diff --git a/docker-config/idp/README.md b/docker-config/idp/README.md
new file mode 100644
index 0000000000..586b9d9953
--- /dev/null
+++ b/docker-config/idp/README.md
@@ -0,0 +1,174 @@
+# Development identity provider test instance for SAML2 authentication
+
+A development SAML2 identity provider is provided that uses SimpleSAMLphp.
+Instructions for utilizing this instance follow.
+
+## Webwork2 Configuration
+
+Copy `/opt/webwork/webwork2/conf/authen_saml2.conf.dist` to
+`/opt/webwork/webwork2/conf/authen_saml2.conf`.
+
+The default `conf/authen_saml2.conf.dist` is configured to use the docker
+identity provider. So for the docker build, it should work as is.
+
+Without the docker build a few changes are needed.
+
+- Find the `$saml2{idps}{default}` setting and change its value to
+ `'http://localhost/simplesaml/module.php/saml/idp/metadata'`.
+- Find the `$saml2{sp}{entity_id}` setting and change its value to
+ `'http://localhost:3000/webwork2/saml2'`.
+- In the `$saml2{sp}{org}` hash change the `url` to `'https://localhost:3000/'`.
+
+The above settings assume you will use `morbo` with the default port. Change
+the port as needed.
+
+## Development IdP test instance with docker
+
+A docker service that implements a SAML2 identity provider is provided in the
+`docker-compose.yml.dist` file. To start this identity provider along with the
+rest of webwork2, add the `--profile saml2dev` argument to docker compose as in
+the following exmaple.
+
+```bash
+docker compose --profile saml2dev up
+```
+
+Without the profile argument, the identity provider services do not start.
+
+Stop all docker services with
+
+```bash
+docker compose --profile saml2dev down
+```
+
+## Development IdP test instance without docker
+
+Effective development is not done with docker. So it is usually more useful to
+set up an identity provider without docker. The following instructions are for
+Ubuntu 24.04, but could be adapted for other operating systems.
+
+A web server and php are needed to serve the SimpleSAMLphp files. Install these
+and other dependencies with:
+
+```bash
+sudo apt install \
+ apache2 php php-ldap php-zip php-xml php-curl php-sqlite3 php-fpm \
+ composer
+```
+
+Now download the SimpleSAMLphp source, install php dependencies, install the
+SimpleSAMLphp metarefresh module, and set file permissions with
+
+```bash
+cd /var/www
+sudo mkdir simplesamlphp /var/cache/simplesamlphp
+sudo chown $USER:www-data simplesamlphp
+sudo chown www-data /var/cache/simplesamlphp
+git clone --branch v2.2.1 https://github.com/simplesamlphp/simplesamlphp.git
+sudo chown -R $USER:www-data simplesamlphp
+sudo chmod -R g+w simplesamlphp
+cd simplesamlphp
+composer install
+composer require simplesamlphp/simplesamlphp-module-metarefresh
+```
+
+Next, generate certificates for the SimpleSAMLphp identity provider and make
+them owned by the `www-data` user with
+
+```bash
+cd /var/www/simplesamlphp/cert
+openssl req -newkey rsa:3072 -new -x509 -days 3652 -nodes \
+ -out server.crt -keyout server.pem \
+ -subj "/C=US/ST=New York/L=Rochester/O=WeBWorK/CN=idp.webwork2"
+sudo chown www-data:www-data server.crt server.pem
+```
+
+Next, copy the `idp` configuration files from `docker-config`.
+
+```bash
+cp /opt/webwork/webwork2/docker-config/idp/config/* /var/www/simplesamlphp/config/
+cp /opt/webwork/webwork2/docker-config/idp/metadata/* /var/www/simplesamlphp/metadata/
+```
+
+The configuration files are setup to work with the docker build. So there are
+some changes that are needed.
+
+Edit the file `/var/www/simplesamlphp/config/config.php` and change
+`baseurlpath` to `simplesaml/`.
+
+Edit the file `/var/www/simplesamlphp/metadata/saml20-idp-hosted.php` and change
+the line that reads
+`$metadata['http://localhost:8180/simplesaml'] = [`
+to
+`$metadata['http://localhost/simplesaml'] = [`.
+
+Enable the apache2 idp configuration with
+
+```bash
+sudo cp /opt/webwork/webwork2/docker-config/idp/idp.apache2.conf /etc/apache2/conf-available
+sudo a2enconf idp.apache2 php8.3-fpm
+```
+
+Edit the file `/etc/apache2/conf-available/idp.apache2.conf` and add the line
+`SetEnv SP_METADATA_URL http://localhost:3000/webwork2/saml2/metadata` to the
+beginning of the file. This again assumes you will use `morbo` with the default
+port, so change the port if necessary.
+
+Restart (or start) apache2 with `sudo systemctl restart apache2`.
+
+The SimpleSAMLphp identity provider needs to fetch webwork2's service provider
+metadata. For this execute
+
+```bash
+curl -f http://localhost/simplesaml/module.php/cron/run/metarefresh/webwork2
+```
+
+That is done automatically with the docker build. The command usually only
+needs to be done once, but may need to be run again if settings are changed.
+
+## Identity provider administration
+
+The identity provider has an admin interface. You can login to the docker
+instance with the password 'admin' at
+`http://localhost:8180/simplesaml/module.php/admin/federation`
+or without docker at
+`http://localhost/simplesaml/module.php/admin/federation`.
+
+The admin interface lets you check if the identity provider has properly
+registered the webwork2 service provider 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.
+
+## Single sign-on users
+
+The following single sign-on accounts are preconfigured:
+
+- Username: student01, Password: student01
+- Username: instructor01, Password: instructor01
+- Username: staff01, Password: staff01
+
+You can add more accounts to the `docker-config/idp/config/authsources.php` file
+in the `example-userpass` section. If using docker the identity provider, the
+image will need to be rebuilt for the changes to take effect.
+
+## Troubleshooting
+
+### "Error retrieving metadata"
+
+This error message indicates that the Saml2 authentication module wasn't able to
+fetch the metadata from the identity provider metadata URL. Make sure the
+identity provider is accessible to webwork2.
+
+### User not found in course
+
+The user was verified by the identity provider but did not have a corresponding
+user account in the Webwork course. The Webwork user account needs to be created
+separately as the Saml2 autentication module does not do user provisioning.
+
+### The WeBWorK service provider does not appear in the service provider Federation tab
+
+This can occur when using the docker identity provider service because Webwork's
+first startup can be slow enough that the IdP wasn't able to successfully fetch
+metadata from the webwork2 metadata URL. Restarting everything should fix this.
diff --git a/docker-config/idp/certs/saml.crt b/docker-config/idp/certs/saml.crt
new file mode 100644
index 0000000000..ca2f952c0f
--- /dev/null
+++ b/docker-config/idp/certs/saml.crt
@@ -0,0 +1,29 @@
+-----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-----
diff --git a/docker-config/idp/certs/saml.pem b/docker-config/idp/certs/saml.pem
new file mode 100644
index 0000000000..65accf00b2
--- /dev/null
+++ b/docker-config/idp/certs/saml.pem
@@ -0,0 +1,40 @@
+-----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/idp/config/authsources.php b/docker-config/idp/config/authsources.php
new file mode 100644
index 0000000000..03740f20bf
--- /dev/null
+++ b/docker-config/idp/config/authsources.php
@@ -0,0 +1,354 @@
+ [
+ // 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..3ecbe1d3ad
--- /dev/null
+++ b/docker-config/idp/config/config.php
@@ -0,0 +1,1301 @@
+ 'http://localhost:8180/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/New_York',
+
+ /**********************************
+ | 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..a05be61da2
--- /dev/null
+++ b/docker-config/idp/config/module_cron.php
@@ -0,0 +1,8 @@
+ 'webwork2',
+ 'allowed_tags' => ['metarefresh'],
+ '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..1b2cf60d61
--- /dev/null
+++ b/docker-config/idp/config/module_metarefresh.php
@@ -0,0 +1,21 @@
+ [
+ 'webwork2' => [
+ 'cron' => ['metarefresh'],
+ 'sources' => [
+ ['src' => $metadataURL]
+ ],
+ 'expiresAfter' => 60 * 60 * 24 * 365 * 10, // 10 years, basically never
+ 'outputDir' => 'metadata/metarefresh-webwork/',
+ 'outputFormat' => 'flatfile',
+ ]
+ ]
+];
diff --git a/docker-config/idp/idp.apache2.conf b/docker-config/idp/idp.apache2.conf
new file mode 100644
index 0000000000..5f2e656ebe
--- /dev/null
+++ b/docker-config/idp/idp.apache2.conf
@@ -0,0 +1,7 @@
+SetEnv SIMPLESAMLPHP_CONFIG_DIR /var/www/simplesamlphp/config
+
+Alias /simplesaml /var/www/simplesamlphp/public
+
+
+ Require all granted
+
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/WeBWorK.pm b/lib/WeBWorK.pm
index 0ac5eeba66..319ea19c82 100644
--- a/lib/WeBWorK.pm
+++ b/lib/WeBWorK.pm
@@ -62,28 +62,19 @@ async sub dispatch ($c) {
# Note that this is Time::HiRes's time, which gives floating point values.
$c->submitTime(time);
- my $method = $c->req->method;
- my $location = $c->location;
- my $uri = $c->url_for;
- my $args = $c->req->params->to_string || '';
+ my $method = $c->req->method;
+ my $uri = $c->url_for;
+ my $args = $c->req->params->to_string || '';
debug("\n\n===> Begin " . __PACKAGE__ . "::dispatch() <===\n\n");
- debug("Hi, I'm the new dispatcher!\n");
debug(("-" x 80) . "\n");
- debug("Okay, I got some basic information:\n");
- debug("The site location is $location\n");
debug("The request method is $method\n");
debug("The URI is $uri\n");
debug("The argument string is $args\n");
debug(('-' x 80) . "\n");
- my ($path) = $uri =~ m/$location(.*)/;
- $path .= '/' if $path !~ m(/$);
- debug("The path is $path\n");
-
debug("The current route is " . $c->current_route . "\n");
- debug("Here is some information about this route:\n");
my $displayModule = ref $c;
my %routeCaptures = %{ $c->stash->{'mojo.captures'} };
@@ -96,8 +87,6 @@ async sub dispatch ($c) {
debug(('-' x 80) . "\n");
- debug("Now we want to look at the parameters we got.\n");
-
debug("The raw params:\n");
for my $key ($c->param) {
# Make it so we dont debug plain text passwords
@@ -122,7 +111,6 @@ async sub dispatch ($c) {
$c->initializeRoute(\%routeCaptures) if $c->can('initializeRoute');
# Create Course Environment
- debug("We need to get a course environment (with or without a courseID!)\n");
my $ce = eval { WeBWorK::CourseEnvironment->new({ courseName => $routeCaptures{courseID} }) };
$@ and die "Failed to initialize course environment: $@\n";
debug("Here's the course environment: $ce\n");
@@ -164,12 +152,14 @@ async sub dispatch ($c) {
if ($routeCaptures{courseID}) {
debug("We got a courseID from the route, now we can do some stuff:\n");
+ # This route could have the courseID set, but does not need authentication.
+ return 1 if $c->current_route eq 'saml2_metadata';
+
return (0, 'This course does not exist.')
unless (-e $ce->{courseDirs}{root}
|| -e "$ce->{webwork_courses_dir}/$ce->{admin_course_id}/archives/$routeCaptures{courseID}.tar.gz");
return (0, 'This course has been archived and closed.') unless -e $ce->{courseDirs}{root};
- debug("...we can create a database object...\n");
my $db = WeBWorK::DB->new($ce->{dbLayout});
debug("(here's the DB handle: $db)\n");
$c->db($db);
diff --git a/lib/WeBWorK/Authen/Saml2.pm b/lib/WeBWorK/Authen/Saml2.pm
new file mode 100644
index 0000000000..83de834738
--- /dev/null
+++ b/lib/WeBWorK/Authen/Saml2.pm
@@ -0,0 +1,348 @@
+################################################################################
+# 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 Mojo::File qw(path tempfile);
+use Mojo::JSON qw(encode_json);
+use Mojo::UserAgent;
+use Net::SAML2::IdP;
+use Net::SAML2::SP;
+use URN::OASIS::SAML2 qw(BINDING_HTTP_POST BINDING_HTTP_REDIRECT);
+use Net::SAML2::Binding::POST;
+use Net::SAML2::Protocol::Assertion;
+
+use WeBWorK::Debug qw(debug);
+use WeBWorK::Authen::LTIAdvanced::Nonce;
+
+=head1 NAME
+
+WeBWorK::Authen::Saml2 - Authenticate using a SAML2 identity provider.
+
+=cut
+
+sub request_has_data_for_this_verification_module ($self) {
+ my $c = $self->{c};
+
+ # Skip if the bypass_query param is set.
+ if ($c->ce->{saml2}{bypass_query} && $c->param($c->ce->{saml2}{bypass_query})) {
+ debug('Saml2 authen module bypass detected. Going to next authentication module.');
+ return 0;
+ }
+
+ return 1;
+}
+
+sub verify ($self) {
+ my $result = $self->SUPER::verify;
+ my $c = $self->{c};
+
+ if ($c->current_route eq 'saml2_acs') {
+ # Transfer the saml2_nameid and saml2_session to the webwork session.
+ # These are used to logout of the identity provider if that is configured.
+ $self->session->{saml2_nameid} = $c->stash->{saml2_nameid} if $c->stash->{saml2_nameid};
+ $self->session->{saml2_session} = $c->stash->{saml2_session} if $c->stash->{saml2_session};
+
+ # If two factor verification is needed, defer that until after redirecting to the course route.
+ if ($c->stash->{saml2_redirect} && $self->session->{two_factor_verification_needed}) {
+ $self->session->{two_factor_verification_needed_after_redirect} =
+ delete $self->session->{two_factor_verification_needed};
+ return 1;
+ }
+ }
+
+ return $result;
+}
+
+sub do_verify ($self) {
+ my $c = $self->{c};
+ my $ce = $c->ce;
+
+ $self->{external_auth} = 1 if $ce->two_factor_authentication_enabled && $ce->{saml2}{twoFAOnlyWithBypass};
+
+ if ($c->current_route eq 'saml2_acs') {
+ debug('Verifying Saml2 assertion');
+
+ my $idpCertificateFile = $self->idp(1);
+ unless ($idpCertificateFile) {
+ $c->stash->{authen_error} = $c->maketext(
+ 'An internal server error occured. Please contact the system administrator for assistance.');
+ return 0;
+ }
+
+ # Verify that the response is signed by the identity provider and decode it.
+ my $decodedXml = Net::SAML2::Binding::POST->new(cacert => $idpCertificateFile->to_string)
+ ->handle_response($c->stash->{saml2}{samlResp});
+ my $assertion = Net::SAML2::Protocol::Assertion->new_from_xml(
+ xml => $decodedXml,
+ key_file => $self->spKeyFile->to_string
+ );
+
+ # Get the database key containing the authReqId that was generated before redirecting to the identity provider.
+ my $authReqIdKey = $c->db->getKey($assertion->in_response_to);
+ unless ($authReqIdKey) {
+ $c->stash->{authen_error} = $c->maketext('Invalid user ID or password.');
+ debug('Invalid request id in response. Possible CSFR.');
+ return 0;
+ }
+ eval { $c->db->deleteKey($authReqIdKey->user_id) }; # Delete the key to avoid replay.
+
+ # Verify that the response has the same authReqId which means it's responding to the authentication request
+ # generated by webwork2. This also checks that timestamps are valid.
+ my $valid = $assertion->valid($ce->{saml2}{sp}{entity_id}, $authReqIdKey->user_id);
+ unless ($valid) {
+ $c->stash->{authen_error} = $c->maketext('Invalid user ID or password.');
+ debug('Bad timestamp or issuer');
+ return 0;
+ }
+
+ debug('Got valid response and looking for username.');
+ my $userId = $self->getUserId($ce->{saml2}{sp}{attributes}, $assertion);
+ if ($userId) {
+ debug("Got username $userId");
+
+ $c->authen->{saml2UserId} = $userId;
+ if ($self->SUPER::do_verify) {
+ # The user and key need to be set before systemLink is called. They are only used if
+ # $session_management_via is 'key'.
+ $c->param('user', $userId);
+ $c->param('key', $self->{session_key});
+ $c->stash->{saml2_redirect} = $c->systemLink($c->url_for($c->stash->{saml2}{relayState}{url}));
+
+ # Save these in the stash for now. They will be transferred to the session after it has been created.
+ $c->stash->{saml2_nameid} = $assertion->nameid;
+ $c->stash->{saml2_session} = $assertion->{session};
+
+ return 1;
+ }
+ }
+ $c->stash->{authen_error} = $c->maketext('User not found in course.');
+ debug('Unauthorized - User not found in ' . $c->stash->{courseID});
+ return 0;
+ }
+
+ # If there is an existing session, then control will be passed to the authen base class.
+ if ($ce->{session_management_via} eq 'session_cookie') {
+ my ($cookieUser) = $self->fetchCookie;
+ $self->{isLoggedIn} = 1 if defined $cookieUser;
+ } elsif ($c->param('user')) {
+ my $key = $c->db->getKey($c->param('user'));
+ $self->{isLoggedIn} = 1 if $key;
+ }
+
+ if ($self->{isLoggedIn}) {
+ debug('User signed in or was previously signed in. Saml2 passing control back to the authen base class.');
+
+ # There was a successful saml response or the user was already logged in.
+ # So hand off to the authen base class to verify the user and manage the session.
+ my $result = $self->SUPER::do_verify;
+
+ $self->session->{two_factor_verification_needed} =
+ delete $self->session->{two_factor_verification_needed_after_redirect}
+ if $self->session->{two_factor_verification_needed_after_redirect};
+
+ return $result;
+ }
+
+ # This occurs if the user clicks the logout button when the identity provider session has timed out, but the
+ # webwork2 session is still active. In this case return 0 so that the logged out page is shown anyway.
+ return 0 if $c->current_route eq 'logout';
+
+ # The user doesn't have an existing session, so redirect to the identity provider for login.
+ $self->sendLoginRequest;
+
+ return 0;
+}
+
+sub sp ($self) {
+ my $c = $self->{c};
+ return $c->stash->{sp} if $c->stash->{sp};
+
+ my $ce = $c->ce;
+
+ my $spCertificateFile = path($ce->{saml2}{sp}{certificate_file});
+ $spCertificateFile = $c->app->home->child($spCertificateFile) unless $spCertificateFile->is_abs;
+
+ $c->stash->{sp} = Net::SAML2::SP->new(
+ issuer => $ce->{saml2}{sp}{entity_id},
+ url => $ce->{server_root_url} . $c->url_for('root'),
+ error_url => $ce->{server_root_url} . $c->url_for('saml2_error'),
+ cert => $spCertificateFile->to_string,
+ key => $self->spKeyFile->to_string,
+ org_contact => $ce->{saml2}{sp}{org}{contact},
+ org_name => $ce->{saml2}{sp}{org}{name},
+ org_url => $ce->{saml2}{sp}{org}{url},
+ org_display_name => $ce->{saml2}{sp}{org}{display_name},
+ assertion_consumer_service => [ {
+ Binding => BINDING_HTTP_POST,
+ Location => $ce->{server_root_url} . $c->url_for('saml2_acs'),
+ isDefault => 'true',
+ } ],
+ $ce->{saml2}{sp}{enable_sp_initiated_logout}
+ ? (
+ single_logout_service => [ {
+ Binding => BINDING_HTTP_POST,
+ Location => $ce->{server_root_url} . $c->url_for('saml2_logout')
+ } ]
+ )
+ : ()
+ );
+
+ return $c->stash->{sp};
+}
+
+# The first time this method is executed for a given identity provider, the metadata file is retrieved from the metadata
+# URL. It is then saved in the $ce->{saml2}{active_idp} subdirectory of $ce->{webworkDirs}{DATA}/Saml2IDPs together
+# with the identity provider's signing key which is extracted from the retrieved metadata. On later requests the
+# metadata and certificate are used from the saved files. This prevents the need to retrieve the metadata on every
+# login request.
+sub idp ($self, $ceritificateOnly = 0) {
+ if (!$self->{idp_certificate_file} || !$self->{idp}) {
+ my $ce = $self->{c}->ce;
+
+ my $saml2IDPDir = path("$ce->{webworkDirs}{DATA}/Saml2IDPs")->child($ce->{saml2}{active_idp});
+ $saml2IDPDir->make_path;
+
+ my $metadataXMLFile = $saml2IDPDir->child('metadata.xml');
+ my $certificateFile = $saml2IDPDir->child('cacert.crt');
+
+ if (-r $metadataXMLFile && -r $certificateFile) {
+ $self->{idp} =
+ Net::SAML2::IdP->new_from_xml(xml => $metadataXMLFile->slurp, cacert => $certificateFile->to_string);
+ $self->{idp_certificate_file} = $certificateFile;
+ } else {
+ my $response = Mojo::UserAgent->new->get($ce->{saml2}{idps}{ $ce->{saml2}{active_idp} })->result;
+ if ($response->is_success) {
+ my $metadataXML = $response->body;
+ $metadataXMLFile->spew($metadataXML);
+ $self->{idp} = Net::SAML2::IdP->new_from_xml(xml => $metadataXML);
+ $certificateFile->spew($self->{idp}->cert('signing')->[0]);
+ $self->{idp_certificate_file} = $certificateFile;
+ } else {
+ debug("Unable to retrieve metadata from identity provider $ce->{saml2}{active_idp} with "
+ . "metadata URL $ce->{samle}{idps}{$ce->{saml2}{active_idp}}");
+ }
+ }
+ }
+
+ return $self->{idp_certificate_file} if $ceritificateOnly;
+ return $self->{idp};
+}
+
+sub spKeyFile ($self) {
+ my $c = $self->{c};
+ return $self->{spKeyFile} if $self->{spKeyFile};
+ $self->{spKeyFile} = path($c->ce->{saml2}{sp}{private_key_file});
+ $self->{spKeyFile} = $c->app->home->child($self->{spKeyFile}) unless $self->{spKeyFile}->is_abs;
+ return $self->{spKeyFile};
+}
+
+sub sendLoginRequest ($self) {
+ my $c = $self->{c};
+ my $ce = $c->ce;
+
+ my $idp = $self->idp;
+ unless ($idp) {
+ $c->stash->{authen_error} =
+ $c->maketext('An internal server error occured. Please contact the system administrator for assistance.');
+ return 0;
+ }
+
+ my $authReq = $self->sp->authn_request($idp->sso_url(BINDING_HTTP_REDIRECT));
+
+ # Get rid of stale request ids in the database. This borrows the maybe_purge_nonces method from the
+ # WeBWorK::Authen::LTIAdvanced::Nonce package.
+ WeBWorK::Authen::LTIAdvanced::Nonce->new($c, '', 0)->maybe_purge_nonces;
+
+ # The request id needs to be stored so that it can be verified in the identity provider response.
+ # This uses the "nonce" hack to store the request id in the key table.
+ my $key = $c->db->newKey({ user_id => $authReq->id, timestamp => time, key => 'nonce' });
+ eval { $c->db->deleteKey($authReq->id) };
+ eval { $c->db->addKey($key) };
+
+ # The second argument of the sign method contains info that the identity provider relays back.
+ # This information is used to send the user to the right place after login.
+ debug('Redirecting user to the identity provider');
+ $self->{redirect} = $self->sp->sso_redirect_binding($idp, 'SAMLRequest')
+ ->sign($authReq->as_xml, encode_json({ course => $ce->{courseName}, url => $c->req->url->to_string }));
+ return;
+}
+
+sub logout_user ($self) {
+ my $ce = $self->{c}->ce;
+ if ($ce->{saml2}{sp}{enable_sp_initiated_logout}
+ && defined $self->session->{saml2_nameid}
+ && defined $self->session->{saml2_session})
+ {
+ my $idp = $self->idp;
+ return unless $idp;
+
+ my $logoutReq = $self->sp->logout_request(
+ $idp->slo_url(BINDING_HTTP_REDIRECT), $self->session->{saml2_nameid},
+ $idp->format || undef, $self->session->{saml2_session}
+ );
+
+ debug('Redirecting user to the identity provider for logout');
+ $self->{redirect} = $self->sp->slo_redirect_binding($idp, 'SAMLRequest')
+ ->sign($logoutReq->as_xml, encode_json({ course => $ce->{courseName} }));
+ }
+ return;
+}
+
+sub getUserId ($self, $attributeKeys, $assertion) {
+ my $ce = $self->{c}->ce;
+ my $db = $self->{c}->db;
+
+ if ($attributeKeys) {
+ for my $key (@$attributeKeys) {
+ debug("Trying attribute $key for username");
+ my $possibleUserId = $assertion->attributes->{$key}[0];
+ next unless $possibleUserId;
+ 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 found.');
+ return;
+}
+
+sub get_credentials ($self) {
+ if ($self->{saml2UserId}) {
+ # User has been authenticated with the identity provider.
+ $self->{user_id} = $self->{saml2UserId};
+ $self->{login_type} = 'normal';
+ $self->{credential_source} = 'SAML2';
+ $self->{initial_login} = 1;
+ debug('credential source: "SAML2", user: "', $self->{user_id}, '"');
+ return 1;
+ }
+ return $self->SUPER::get_credentials if $self->{isLoggedIn};
+ return 0;
+}
+
+sub authenticate ($self) {
+ # The identity provider handles authentication, so just return 1.
+ return 1;
+}
+
+1;
diff --git a/lib/WeBWorK/ContentGenerator/Logout.pm b/lib/WeBWorK/ContentGenerator/Logout.pm
index 60629ffcb6..67919656e2 100644
--- a/lib/WeBWorK/ContentGenerator/Logout.pm
+++ b/lib/WeBWorK/ContentGenerator/Logout.pm
@@ -30,7 +30,9 @@ sub pre_header_initialize ($c) {
my $db = $c->db;
my $authen = $c->authen;
- my $userID = $c->param('user_id');
+ # Do any special processing needed by external authentication. This is done before
+ # the session is killed in case the authentication module needs access to it.
+ $authen->logout_user if $authen->can('logout_user');
$authen->killSession;
$authen->WeBWorK::Authen::write_log_entry('LOGGED OUT');
@@ -39,6 +41,8 @@ sub pre_header_initialize ($c) {
# a proctored test. So try and delete the key.
my $proctorID = $c->param('proctor_user');
if ($proctorID) {
+ my $userID = $c->param('user_id');
+
eval { $db->deleteKey("$userID,$proctorID"); };
if ($@) {
$c->addbadmessage("Error when clearing proctor key: $@");
@@ -50,9 +54,6 @@ sub pre_header_initialize ($c) {
}
}
- # Do any special processing needed by external authentication.
- $authen->logout_user if $authen->can('logout_user');
-
$c->reply_with_redirect($authen->{redirect}) if $authen->{redirect};
return;
diff --git a/lib/WeBWorK/ContentGenerator/Saml2.pm b/lib/WeBWorK/ContentGenerator/Saml2.pm
new file mode 100644
index 0000000000..bda1b4aaac
--- /dev/null
+++ b/lib/WeBWorK/ContentGenerator/Saml2.pm
@@ -0,0 +1,60 @@
+################################################################################
+# 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::ContentGenerator::Saml2;
+use Mojo::Base 'WeBWorK::ContentGenerator', -signatures;
+
+use Mojo::JSON qw(decode_json);
+
+use WeBWorK::Debug qw(debug);
+
+sub initializeRoute ($c, $routeCaptures) {
+ if ($c->current_route eq 'saml2_acs') {
+ return unless $c->param('SAMLResponse') && $c->param('RelayState');
+ $c->stash->{saml2}{relayState} = decode_json($c->param('RelayState'));
+ $c->stash->{saml2}{samlResp} = $c->param('SAMLResponse');
+ $routeCaptures->{courseID} = $c->stash->{courseID} = $c->stash->{saml2}{relayState}{course};
+ }
+
+ $routeCaptures->{courseID} = $c->stash->{courseID} = $c->param('courseID')
+ if $c->current_route eq 'saml2_metadata' && $c->param('courseID');
+
+ return;
+}
+
+sub assertionConsumerService ($c) {
+ debug('Authentication succeeded. Redirecting to ' . $c->stash->{saml2_redirect});
+ return $c->redirect_to($c->stash->{saml2_redirect});
+}
+
+sub metadata ($c) {
+ return $c->render(data => 'Internal site configuration error', status => 500) unless $c->authen->can('sp');
+ return $c->render(data => $c->authen->sp->metadata, format => 'xml');
+}
+
+sub errorResponse ($c) {
+ return $c->reply->exception('SAML2 Login Error')->rendered(400);
+}
+
+# When this request comes in the user is actually already signed out of webwork, so this just attempts to redirect back
+# to webwork's logout page for the course. This doesn't verify anything in the response from the identity provider, but
+# hopefully the courseID is found in the relay state so that the user can be redirected to the logout page for the
+# course.
+sub logout ($c) {
+ return $c->render('SAML2 Logout Error', status => 500) unless $c->param('RelayState');
+ return $c->redirect_to($c->url_for('logout', courseID => decode_json($c->param('RelayState'))->{course}));
+}
+
+1;
diff --git a/lib/WeBWorK/Utils/Routes.pm b/lib/WeBWorK/Utils/Routes.pm
index 3197229372..39deece4f6 100644
--- a/lib/WeBWorK/Utils/Routes.pm
+++ b/lib/WeBWorK/Utils/Routes.pm
@@ -39,6 +39,11 @@ PLEASE FOR THE LOVE OF GOD UPDATE THIS IF YOU CHANGE THE ROUTES BELOW!!!
ltiadvantage_keys /ltiadvantage/keys
ltiadvantage_content_selection /ltiadvantage/content_selection
+ saml2_acs /saml2/acs
+ saml2_metadata /saml2/metadata
+ saml2_error /saml2/error
+ saml2_logout /saml2/logout
+
pod_index /pod
pod_viewer /pod/$filePath
@@ -155,6 +160,10 @@ my %routeParameters = (
ltiadvantage_launch
ltiadvantage_keys
ltiadvantage_content_selection
+ saml2_acs
+ saml2_metadata
+ saml2_error
+ saml2_logout
pod_index
sample_problem_index
set_list
@@ -222,6 +231,33 @@ my %routeParameters = (
action => 'content_selection'
},
+ # This route also ends up at the login screen on failure, and the title is not used anywhere else.
+ saml2_acs => {
+ title => x('Login'),
+ module => 'Saml2',
+ path => '/saml2/acs',
+ action => 'assertionConsumerService',
+ methods => ['POST']
+ },
+ saml2_metadata => {
+ title => 'metadata',
+ module => 'Saml2',
+ path => '/saml2/metadata',
+ action => 'metadata'
+ },
+ saml2_error => {
+ title => 'error',
+ module => 'Saml2',
+ path => '/saml2/error',
+ action => 'errorResponse'
+ },
+ saml2_logout => {
+ title => 'logout',
+ module => 'Saml2',
+ path => '/saml2/logout',
+ action => 'logout'
+ },
+
pod_index => {
title => x('POD Index'),
children => [qw(pod_viewer)],
@@ -574,12 +610,13 @@ sub setup_content_generator_routes_recursive {
if ($routeParameters{$child}{children}) {
my $child_route = $route->under($routeParameters{$child}{path}, [ problemID => qr/\d+/ ])->name($child);
- $child_route->any('/')->to("$routeParameters{$child}{module}#$action")->name($child);
+ $child_route->any($routeParameters{$child}{methods} // (), '/')->to("$routeParameters{$child}{module}#$action")
+ ->name($child);
for (@{ $routeParameters{$child}{children} }) {
setup_content_generator_routes_recursive($child_route, $_);
}
} else {
- $route->any($routeParameters{$child}{path}, [ problemID => qr/\d+/ ])
+ $route->any($routeParameters{$child}{methods} // (), $routeParameters{$child}{path}, [ problemID => qr/\d+/ ])
->to("$routeParameters{$child}{module}#$action")->name($child);
}
diff --git a/templates/ContentGenerator/Login.html.ep b/templates/ContentGenerator/Login.html.ep
index 0436697cd5..9031aacee5 100644
--- a/templates/ContentGenerator/Login.html.ep
+++ b/templates/ContentGenerator/Login.html.ep
@@ -6,7 +6,7 @@
%
% my $course = (stash('courseID') // '') =~ s/_/ /gr;
%
-% if ($externalAuth) {
+% if ($ce->{LTI} && $externalAuth) {
% my $LMS = $ce->{LTI}{ $ce->{LTIVersion} }{LMS_url}
% ? link_to($ce->{LTI}{ $ce->{LTIVersion} }{LMS_name} => $ce->{LTI}{ $ce->{LTIVersion} }{LMS_url})
% : $ce->{LTI}{ $ce->{LTIVersion} }{LMS_name};
@@ -24,6 +24,13 @@
tag('strong', $course), $LMS) =%>
% }
+% } elsif ($externalAuth) {
+ % if (stash('authen_error')) {
+
+ <%== maketext(q{This course uses an external authentication system. You've authenticated }
+ . q{through that system, but aren't allowed to log in to this course.}) =%>
+
+ % }
% } else {
<%== maketext('Please enter your username and password for [_1] below:', tag('b', $course)) %>
%
diff --git a/templates/ContentGenerator/Logout.html.ep b/templates/ContentGenerator/Logout.html.ep
index 59c19fb283..ed4d9fd928 100644
--- a/templates/ContentGenerator/Logout.html.ep
+++ b/templates/ContentGenerator/Logout.html.ep
@@ -1,7 +1,7 @@
<%= maketext('You have been logged out of WeBWorK.') =%>
%
% # This should be set in the course environment when a sequence of authentication modules is used.
-% if ($ce->{external_auth} || $authen->{external_auth}) {
+% if ($ce->{LTI} && ($ce->{external_auth} || $authen->{external_auth})) {
<%== maketext(
'The course [_1] uses an external authentication system ([_2]). Please go there to log in again.',
@@ -12,6 +12,9 @@
: $ce->{LTI}{ $ce->{LTIVersion} }{LMS_name}
) =%>
+% } elsif ($ce->{external_auth} || $authen->{external_auth}) {
+ <%== maketext('This course uses an external authentication system. '
+ . 'Please return to its sign in page to log in again.') =%>
% } else {
<%= form_for 'set_list', method => 'POST', begin =%>
<%= hidden_field force_passwd_authen => 1 =%>