This project provides a simple, containerized SAML 2.0 Identity Provider (IdP) using Docker and SimpleSAMLphp. It is designed for local development, allowing developers to build and test SAML-based authentication flows for their applications without needing a complex, production IdP. It stands in for CWL, UBC's Shibboleth IdP, and behaves like it where that matters to an application: each app receives only the attributes it is registered for, and logout messages are signed.
To get the IdP running on your local machine, you will need Docker and Docker Compose.
-
Clone the repository:
git clone https://github.com/ubc/docker-simple-saml cd docker-simple-saml -
Build and Run the Container:
docker-compose up -d --build
This command will build the Docker image, start the container in the background, and generate a self-signed certificate and key in
./cert/if they don't exist.
The SAML IdP will be available at http://localhost:6122/simplesaml/.
The ../saml-test-client/cert mount. docker-compose.yml also mounts ../saml-test-client/cert, a folder beside this clone, so that a newly generated IdP certificate is copied there for the companion saml-test-client. If that folder does not exist, Docker creates it (empty) when the container starts. If you don't use saml-test-client, you can delete that line from docker-compose.yml.
The image has a health check: the container is healthy only when the IdP's metadata answers and carries both its entityID and its signing certificate, and the signing key can be loaded by www-data, the user Apache signs as.
docker-compose ps # STATUS shows (healthy)
docker inspect --format '{{.State.Health.Status}}' <container-name>A fresh container reports starting until the first check passes (about 5 seconds after start on current Docker), then it is re-checked every 30 seconds. The key check matters because metadata needs only the certificate: a key that www-data cannot read (a root-owned cert/server.pem with mode 600 on a Linux host, say) fails every sign-in while the metadata still answers.
The image installs the newest SimpleSAMLphp 2.x every time it is built (simplesamlphp/simplesamlphp:^2.0), on PHP 8.3. To see the version a build installed:
docker exec <container-name> grep -m1 '"version"' /var/www/simplesamlphp/composer.jsonThese are the IdP's endpoints (SimpleSAMLphp 2.x paths):
| URL | |
|---|---|
entityID (the IdP's Issuer) |
http://localhost:6122/simplesaml/saml2/idp/metadata.php |
Single sign-on (entryPoint) |
http://localhost:6122/simplesaml/module.php/saml/idp/singleSignOnService |
Single logout (logoutUrl) |
http://localhost:6122/simplesaml/module.php/saml/idp/singleLogout |
| Metadata | http://localhost:6122/simplesaml/module.php/saml/idp/metadata |
| Signing certificate | ./cert/server.crt (or the X509Certificate in the metadata) |
The older /simplesaml/saml2/idp/SSOService.php, SingleLogoutService.php and metadata.php URLs still work too.
passport-ubcshib users: set entryPoint and logoutUrl yourself. Its LOCAL preset (SAML_ENVIRONMENT=LOCAL) still points at http://localhost:8080 and SimpleSAMLphp 1.x paths, which this IdP does not serve. For example:
new Strategy(
{
issuer: 'http://localhost:8050', // must match an entityID in saml20-sp-remote.php
callbackUrl: 'http://localhost:8050/auth/saml/callback', // must match its ACS
entryPoint: 'http://localhost:6122/simplesaml/module.php/saml/idp/singleSignOnService',
logoutUrl: 'http://localhost:6122/simplesaml/module.php/saml/idp/singleLogout',
cert: fs.readFileSync('path/to/docker-simple-saml/cert/server.crt', 'utf8'),
attributeConfig: ['ubcEduCwlPuid', 'mail', 'eduPersonAffiliation'],
},
verify,
);This IdP sends attributes under their friendly names (uid, mail, ubcEduCwlPuid, ...). Real CWL sends them under OID names (urn:oid:0.9.2342.19200300.100.1.3 for mail, urn:oid:1.3.6.1.4.1.60.6.1.6 for ubcEduCwlPuid, ...). passport-ubcshib's attributeConfig maps both to the friendly names, so the same code works against both.
To test an app against CWL's naming, an SP can opt in to OID names: see Releasing OID names below.
All configuration is handled through files in the config/simplesamlphp/ directory, which is bind-mounted into the container. SimpleSAMLphp reads them on every request, so an edit takes effect without a rebuild.
All optional. Set them in your shell or in a .env file beside docker-compose.yml (git ignores .env); docker-compose.yml passes them to the container. Unset or empty keeps the default.
| Variable | Default | What it sets |
|---|---|---|
IDP_ENTITY_ID |
http://localhost:6122/simplesaml/saml2/idp/metadata.php |
The IdP's entityID, the same whatever host name or port it is reached on. Change it only if your SPs expect another value. |
SSP_ADMIN_PASSWORD |
admin123 |
The password for SimpleSAMLphp's admin pages (/simplesaml/admin/). |
SSP_SECRET_SALT |
yoursupersecuresalthere |
SimpleSAMLphp's secretsalt. |
The defaults are public, which is fine for a local test IdP.
A default set of test users is provided. You can view, edit, or add new users in the following file:
- File:
config/simplesamlphp/authsources.php
It defines 35 test personas: 15 faculty, 15 students, 2 TAs (affiliated as both student and staff) and 3 staff. Most line up with the records the UBC Academic API fake (FakeAcademicAPI) seeds, covering cases such as an instructor who teaches nothing, a student with no email on their academic record, and someone with both an employee and a student number; each persona's comment says which. Every password is the login name (faculty / faculty, bio_student / bio_student, ...). Note that the original three, faculty, student and staff, release uid as faculty-user, student-user and staff-user.
Each persona's eduPersonTargetedId is a fixed placeholder (http://localhost:6122!https://your-app!ID…), not a value computed per SP as a real IdP's would be.
You can authorize your local applications to use this IdP by adding them as Service Providers (SPs). The configuration for allowed SPs is located in:
- File:
config/simplesamlphp/saml20-sp-remote.php
It lists each application's entityID (its issuer), Assertion Consumer Service (ACS) URL, single logout URL and the attributes it receives. The quickest way to add an app that follows the common layout (ACS at /auth/saml/callback, logout at /auth/logout) is a line in $local_apps:
'http://localhost:8090' => 'http://localhost:8090', // entityID => base URLIf its ACS is elsewhere, override it after the loop, as the file already does for http://localhost:6118:
$metadata['http://localhost:8090']['AssertionConsumerService'][0]['Location'] = 'http://localhost:8090/auth/ubcshib/callback';Like real CWL, the IdP releases to each SP only the attributes listed in its entry's attributes (SimpleSAMLphp's core:AttributeLimit, in config.php). Apps in $local_apps receive $default_attributes:
uid, eduPersonAffiliation, eduPersonPrincipalName, givenName, sn, ubcEduCwlPuid, mail
To give one app more, give its entry its own list, for example after the loop:
$metadata['http://localhost:8090']['attributes'] = array_merge( $default_attributes, array( 'cwlLoginName' ) );Two things to avoid: an empty attributes list releases everything, and an entry with no attributes key falls back to the IdP's own list in saml20-idp-hosted.php. If your app reads an attribute it does not receive, add it to its list here. That is also what you will need to ask for when you register the app with CWL.
To have the IdP send an app UBC's real OID attribute names, merge $ubc_oid_attribute_names into its entry, after the entry is defined:
$metadata['http://localhost:8090'] = array_merge( $metadata['http://localhost:8090'], $ubc_oid_attribute_names );The entry's attributes list stays in friendly names: the limit runs first, on friendly names, and the rename to OIDs runs after it. (Doing it the other way round releases nothing at all.) ubcEduCwlPuid maps to urn:oid:1.3.6.1.4.1.60.6.1.6 (attributemap/ubcoid.php); cwlLoginName has no confirmed OID and keeps its friendly name.
With passport-ubcshib, setting privateKeyPath makes your app sign its AuthnRequests. The entries in this file set 'validate.authnrequest' => false, so the IdP does not check that signature and nothing more is needed. To have the IdP check it, set validate.authnrequest to true in the app's entry and give the entry the app's certificate, or every sign-in is refused with Missing certificate in metadata:
$metadata['http://localhost:8090']['validate.authnrequest'] = true;
$metadata['http://localhost:8090']['certData'] = 'MIIC...'; // the app's certificate, base64, no PEM header lines(An entry that leaves validate.authnrequest out gets the same checking as true whenever a request is signed.)
Signing out of an app ends the app's session, not necessarily the IdP's. While the IdP session lasts, the next sign-in at that browser skips the password and returns the same person. Two ways to end it:
- From your app: send the browser through the IdP's single logout, e.g.
http://localhost:6122/simplesaml/module.php/saml/idp/singleLogout?ReturnTo=http://localhost:8050/. The IdP signs the person out of every app that has a logout endpoint, then returns toReturnTo, which may be anylocalhostor127.0.0.1URL, on any port. The IdP's LogoutRequest and LogoutResponse messages are signed, as real CWL's are. - To switch test user by hand: visit
http://localhost:6122/simplesaml/module.php/core/logout/example-userpass(the IdP's front page,http://localhost:6122/simplesaml/, links to it), then sign in from your app again.
Once the container is running, you can test its functionality:
-
IdP-Initiated Login Page:
- Visit
http://localhost:6122/simplesaml/test-idp-init.phpfor a simple test page.
- Visit
-
A test user's attributes:
http://localhost:6122/simplesaml/module.php/admin/test/example-userpassasks for the admin password (admin123unlessSSP_ADMIN_PASSWORDis set), then a test user's login, then shows every attribute that user has. (What an app receives is limited to its entry's list, above.)
-
Viewing Logs:
- Log files are written to the
./log/directory on your local machine. You can view thesimplesamlphp.logfile there to debug any issues.
- Log files are written to the
- IdP Metadata: You can find the IdP's metadata XML at
http://localhost:6122/simplesaml/module.php/saml/idp/metadata(or the olderhttp://localhost:6122/simplesaml/saml2/idp/metadata.php). This URL is typically provided to an SP during configuration.