Skip to content

Commit d0ef071

Browse files
committed
Rewrite SP web, enable logout, document andrvotr demo
1 parent b56288a commit d0ef071

File tree

13 files changed

+156
-165
lines changed

13 files changed

+156
-165
lines changed

README.md

Lines changed: 84 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# SAML local development environment guide
1+
# SAML & Shibboleth dev setup guide
22

33
This guide explains how to install and configure:
44

@@ -622,13 +622,13 @@ Create (or copy from this repo):
622622

623623
TODO /etc/apache2/sites-available/spmellon.conf
624624

625-
TODO /var/www/pyinfo.py
625+
TODO /var/www/sp/sp.py
626626

627-
TODO /var/www/spmellon/index.html
628-
629-
TODO /var/www/spmellon/secret/index.html
627+
> [!NOTE]
628+
> Since both SP and IdP run on the same virtual machine, for convenience, I directly use the path to idp-metadata.xml in the SP config. In production, this XML file would, of course, be copied to the other machine.
630629
631-
(Since both SP and IdP run on the same virtual machine, for convenience, I directly use the path to idp-metadata.xml. In production, this XML file would, of course, be copied to the other machine.)
630+
> [!NOTE]
631+
> This config uses mod_wsgi and a small Python website, but that's just an example. Python just prints the request variables and does not handle any auth. PHP with `phpinfo();`, or just static `secret/index.html`, would work just as well.
632632
633633
Edit both `/opt/idp4/conf/metadata-providers.xml` and `/opt/idp5/conf/metadata-providers.xml` and add the following at the bottom (just above the last line `</MetadataProvider>`):
634634

@@ -661,14 +661,6 @@ Create (or copy from this repo):
661661

662662
TODO /etc/apache2/sites-available/spmellon2.conf
663663

664-
TODO /var/www/pyinfo.py
665-
666-
TODO /var/www/spmellon2/index.html
667-
668-
TODO /var/www/spmellon2/secret/index.html
669-
670-
(Since both SP and IdP run on the same virtual machine, for convenience, I directly use the path to idp-metadata.xml. In production, this XML file would, of course, be copied to the other machine.)
671-
672664
Edit both `/opt/idp4/conf/metadata-providers.xml` and `/opt/idp5/conf/metadata-providers.xml` and add the following at the bottom (just above the last line `</MetadataProvider>`):
673665

674666
```xml
@@ -707,11 +699,7 @@ Create (or copy from this repo):
707699

708700
TODO /etc/apache2/sites-available/spshib.conf
709701

710-
TODO /var/www/pyinfo.py
711-
712-
TODO /var/www/spshib/index.html
713-
714-
TODO /var/www/spshib/secret/index.html
702+
TODO /var/www/sp/sp.py
715703

716704
Edit `/etc/apache2/conf-available/shib.conf` as follows: change `ShibCompatValidUser Off` to `ShibCompatValidUser On`.
717705

@@ -794,6 +782,37 @@ Edit both `/opt/idp4/conf/idp.properties` and `/opt/idp5/conf/idp.properties` as
794782

795783

796784

785+
## Logout
786+
787+
Official docs:
788+
[IDP5 LogoutConfiguration](https://shibboleth.atlassian.net/wiki/spaces/IDP5/pages/3199510118/LogoutConfiguration),
789+
[IDP4 LogoutConfiguration](https://shibboleth.atlassian.net/wiki/spaces/IDP4/pages/1265631719/LogoutConfiguration).
790+
(Completely useless. Even worse than usual.)
791+
792+
Edit `/opt/idp4/metadata/idp-metadata.xml` as follows: uncomment the 4 lines which start with `<SingleLogoutService`.
793+
794+
Edit `/opt/idp5/metadata/idp-metadata.xml` as follows:
795+
796+
- Delete the line `<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://idp.unibatest.internal/idp/profile/SAML2/SOAP/ArtifactResolution" />`, if present. (I think it is a bug in IdP 5.1.3, because `SAML2/SOAP/ArtifactResolution` should be `<ArtifactResolutionService>`, not `<SingleLogoutService>`.)
797+
- Add these lines between the last `</md:KeyDescriptor>` and the first `<md:SingleSignOnService...>` (mod_shib cares about element order):
798+
```xml
799+
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://idp.unibatest.internal/idp/profile/SAML2/POST/SLO"/>
800+
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://idp.unibatest.internal/idp/profile/SAML2/Redirect/SLO"/>
801+
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign" Location="https://idp.unibatest.internal/idp/profile/SAML2/POST-SimpleSign/SLO"/>
802+
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://idp.unibatest.internal:8443/idp/profile/SAML2/SOAP/SLO"/>
803+
```
804+
805+
I have no idea how anyone is supposed to discover this. The official docs did not even mention editing IdP metadata. At least for IdP 4 the installer wrote that comment. In IdP 5 there is not even that.
806+
807+
Restart the services again. At this point you should be able to log out. If you are logged in to multiple SPs, the IdP will show a "Logout Locally" and "Logout Globally" button.
808+
809+
It has a few unexpected quirks:
810+
811+
- The IdP does not redirect the user back to the initiating SP. It shows a logout message and stays there. A SAML logout response is sent to the SP's logout service handler inside a hidden iframe.
812+
- If you press "Logout Locally" and later try to logout from some other service which did not log out, it will fail with an error because the IdP session does not exist anymore.
813+
814+
815+
797816
## Andrvotr development
798817

799818
This section is specific to working on the [Andrvotr](https://github.com/fmfi-svt/andrvotr) plugin.
@@ -808,7 +827,7 @@ echo 'PATH=$HOME/apache-maven-3.9.9/bin:$PATH' >> .bashrc
808827
exec bash
809828
```
810829

811-
Follow the procedure in the Andrvotr README to create a GPG key.
830+
Follow [the procedure in the Andrvotr README](https://github.com/fmfi-svt/andrvotr/blob/idp5/README.md#building-from-source) to create a GPG key.
812831

813832
Run this so that `plugin.sh` is able read the plugin file.
814833

@@ -836,11 +855,11 @@ time PATH=/usr/lib/jvm/java-11-amazon-corretto/bin:$PATH GNUPGHOME=../gpgdir MAV
836855

837856
The long command builds the plugin, installs it, restarts the IdP, and waits for it to start.
838857

839-
Always remember to build from the correct branch, and to run `git clean -fdX` when you switch.
858+
Always remember to build from the correct branch, and to run `git clean -fdX` when you switch, because Maven gets confused by stale build outputs.
840859

841-
Edit both `/opt/idp4/conf/attribute-resolver.xml` and `/opt/idp5/conf/attribute-resolver.xml` as described in the Andrvotr README.
860+
Edit both `/opt/idp4/conf/attribute-resolver.xml` and `/opt/idp5/conf/attribute-resolver.xml` as described in the [Andrvotr README](https://github.com/fmfi-svt/andrvotr/blob/idp5/README.md#building-from-source).
842861

843-
Edit both `/opt/idp4/conf/attribute-filter.xml` and `/opt/idp5/conf/attribute-filter.xml` as described in the Andrvotr README.
862+
Edit both `/opt/idp4/conf/attribute-filter.xml` and `/opt/idp5/conf/attribute-filter.xml` as described in the [Andrvotr README](https://github.com/fmfi-svt/andrvotr/blob/idp5/README.md#building-from-source).
844863

845864
Edit both `/opt/idp4/conf/idp.properties` and `/opt/idp5/conf/idp.properties` and append this:
846865

@@ -864,54 +883,63 @@ Edit `/etc/shibboleth/attribute-map.xml` and add this line just before `</Attrib
864883
<Attribute name="tag:fmfi-svt.github.io,2024:andrvotr-authority-token" id="ANDRVOTR_AUTHORITY_TOKEN" />
865884
```
866885

867-
TODO: Explain how to demo.
886+
At this point, you should see the new SAML attribute in the server environment when you log in to `spmellon` or `spshib`.
868887

869-
When needed, you can enable maximum logging in `idp.properties` with this:
888+
Edit `/etc/hosts` and add this line:
870889

871-
```ini
872-
idp.loglevel.idp=TRACE
873-
idp.loglevel.ldap=TRACE
874-
idp.loglevel.messages=TRACE
875-
idp.loglevel.encryption=TRACE
876-
idp.loglevel.opensaml=TRACE
877-
idp.loglevel.shared=TRACE
878-
idp.loglevel.props=TRACE
879-
idp.loglevel.httpclient=TRACE
880-
idp.loglevel.spring=TRACE
881-
idp.loglevel.container=TRACE
882-
idp.loglevel.xmlsec=TRACE
883-
idp.loglevel.root=TRACE
890+
```
891+
127.0.0.1 idp.unibatest.internal spmellon.unibatest.internal spmellon2.unibatest.internal spshib.unibatest.internal
884892
```
885893

894+
(Editing `/etc/hosts` wasn't needed until now, for normal SAML flows. But Andrvotr requires it.)
886895

896+
Run:
887897

898+
```shell
899+
curl -LsSf https://astral.sh/uv/install.sh | sh
900+
```
888901

902+
Sign in to spmellon.unibatest.internal in a browser. Find the value of `'MELLON_tag:fmfi-svt.github.io,2024:andrvotr-authority-token'` in the WSGI environment. Run:
889903

904+
```shell
905+
read aat
906+
# paste the value
907+
PYTHONWARNINGS=ignore VERIFY_TLS_CERTS=false ./demo/demo.py 'https://spshib.unibatest.internal/secret/' "https://spmellon.unibatest.internal/mellon/metadata" "secretmellonkey" "$aat"
908+
```
890909

910+
It should print the "spshib SECRET, logged IN" page.
891911

912+
Now try with `'https://spmellon2.unibatest.internal/secret/'` instead of `'https://spshib.unibatest.internal/secret/'`. It should print an error because spmellon to spmellon2 is not in andrvotr.allowedConnections. The message should contain `@AllowedConnectionCheckFailure`.
892913

893-
## Unsorted
914+
Sign in to spshib.unibatest.internal in a browser. Find the value of `'ANDRVOTR_AUTHORITY_TOKEN'` in the WSGI environment. Run:
894915

895916
```shell
896-
chmod 755 ~
917+
read aat
918+
# paste the value
919+
PYTHONWARNINGS=ignore VERIFY_TLS_CERTS=false ./demo/demo.py 'https://spmellon2.unibatest.internal/secret/' "https://spshib.unibatest.internal/shibboleth" "secretshibkey" "$aat"
897920
```
898921

899-
Because of andrvotr/fabricate I had to also add idp.unibatest.internal to /etc/hosts (`127.0.1.1 samltest idp.unibatest.internal`).
922+
It should print the "spmellon2 SECRET, logged IN" page.
900923

901-
Edit /opt/idp5/conf/idp.properties and append at the bottom:
924+
### Verbose IdP logging
902925

903-
```ini
904-
andrvotr.httpclient.connectionDisregardTLSCertificate=true
905-
```
906-
907-
Run:
926+
When needed, you can enable maximum logging in `idp.properties` with this:
908927

909-
```shell
910-
curl -LsSf https://astral.sh/uv/install.sh | sh
928+
```ini
929+
idp.loglevel.idp=TRACE
930+
idp.loglevel.ldap=TRACE
931+
idp.loglevel.messages=TRACE
932+
idp.loglevel.encryption=TRACE
933+
idp.loglevel.opensaml=TRACE
934+
idp.loglevel.shared=TRACE
935+
idp.loglevel.props=TRACE
936+
idp.loglevel.httpclient=TRACE
937+
idp.loglevel.spring=TRACE
938+
idp.loglevel.container=TRACE
939+
idp.loglevel.xmlsec=TRACE
940+
idp.loglevel.root=TRACE
911941
```
912942

913-
TODO: maximum logging in idp.properties
914-
915943

916944

917945
## Miscellaneous SAML debugging commands
@@ -966,13 +994,15 @@ For example, they appear as opaque NameID values, as entries in `localStorage` i
966994
If you have the private keys of the IdP, you can decrypt them like this:
967995

968996
```shell
969-
sudo -u idp /opt/idp5/bin/runclass.sh -Didp.home=/opt/idp5 net.shibboleth.idp.cli.DataSealerCLI --verbose net/shibboleth/idp/conf/sealer.xml dec "$str"
997+
sudo -u idp JAVA_OPTS=-Didp.home=/opt/idp5 /opt/idp5/bin/sealer.sh --verbose net/shibboleth/idp/conf/sealer.xml dec "$str"
970998
```
971999

9721000
If decrypting fails (e.g., because the timestamp inside the encrypted value has expired), it will display a misleading error: "Unable to access DataSealer from Spring context". You need the `--verbose` option to show the real error.
9731001

974-
`bin/sealer.sh` only works if it's installed in the default path `/opt/shibboleth-idp`. That's why we must use `runclass.sh ... DataSealerCLI` as a workaround.
1002+
IdP 4 `sealer.sh` does not know `JAVA_OPTS`, so if it's installed in a custom path other than `/opt/shibboleth-idp`, you must run:
9751003

976-
TODO: `JAVA_OPTS=-Didp.home=/opt/idp5` should work. But only on IdP 5. :(
1004+
```shell
1005+
sudo -u idp /opt/idp4/bin/runclass.sh -Didp.home=/opt/idp4 net.shibboleth.idp.cli.DataSealerCLI --verbose net/shibboleth/idp/conf/sealer.xml dec "$str"
1006+
```
9771007

9781008
The value `net/shibboleth/idp/conf/sealer.xml` is undocumented; it was discovered via grepping. It won’t work without it.

etc/apache2/sites-available/spmellon.conf

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@
33
SSLEngine on
44
SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
55
SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
6-
DocumentRoot /var/www/spmellon
6+
DocumentRoot /nonexistent
77
ErrorLog ${APACHE_LOG_DIR}/spmellon-error.log
88
CustomLog ${APACHE_LOG_DIR}/spmellon-access.log combined
9-
WSGIScriptAlias /pyinfo /var/www/pyinfo.py process-group=spmellonpy
10-
WSGIScriptAlias /secret/pyinfo /var/www/pyinfo.py process-group=spmellonpy
11-
WSGIDaemonProcess spmellonpy processes=1 threads=1
9+
WSGIScriptAlias / /var/www/sp/sp.py process-group=spmellonpy
10+
WSGIDaemonProcess spmellonpy processes=1 threads=1 home=/var/www/sp
1211
WSGIApplicationGroup %{GLOBAL}
1312
<Location />
1413
MellonEnable "info"

etc/apache2/sites-available/spmellon2.conf

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@
33
SSLEngine on
44
SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
55
SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
6-
DocumentRoot /var/www/spmellon2
6+
DocumentRoot /nonexistent
77
ErrorLog ${APACHE_LOG_DIR}/spmellon2-error.log
88
CustomLog ${APACHE_LOG_DIR}/spmellon2-access.log combined
9-
WSGIScriptAlias /pyinfo /var/www/pyinfo.py process-group=spmellon2py
10-
WSGIScriptAlias /secret/pyinfo /var/www/pyinfo.py process-group=spmellon2py
11-
WSGIDaemonProcess spmellon2py processes=1 threads=1
9+
WSGIScriptAlias / /var/www/sp/sp.py process-group=spmellon2py
10+
WSGIDaemonProcess spmellon2py processes=1 threads=1 home=/var/www/sp
1211
WSGIApplicationGroup %{GLOBAL}
1312
<Location />
1413
MellonEnable "info"

etc/apache2/sites-available/spshib.conf

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@
33
SSLEngine on
44
SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
55
SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
6-
DocumentRoot /var/www/spshib
6+
DocumentRoot /nonexistent
77
ErrorLog ${APACHE_LOG_DIR}/spshib-error.log
88
CustomLog ${APACHE_LOG_DIR}/spshib-access.log combined
9-
WSGIScriptAlias /pyinfo /var/www/pyinfo.py process-group=spshibpy
10-
WSGIScriptAlias /secret/pyinfo /var/www/pyinfo.py process-group=spshibpy
11-
WSGIDaemonProcess spshibpy processes=1 threads=1
9+
WSGIScriptAlias / /var/www/sp/sp.py process-group=spshibpy
10+
WSGIDaemonProcess spshibpy processes=1 threads=1 home=/var/www/sp
1211
WSGIApplicationGroup %{GLOBAL}
1312
<Location />
1413
AuthType Shibboleth

system-to-repo

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ rm -rf etc opt var
77
for f in \
88
etc/systemd/system/idp.service \
99
etc/apache2/sites-available \
10-
var/www \
10+
var/www/sp \
1111
opt/idpswitch \
1212
;
1313
do
1414
mkdir -p "${f%/*}"
1515
cp -av "/$f" "$f"
1616
done
1717

18-
rm -rf etc/apache2/sites-available/*default* var/www/html opt/idpswitch/active
18+
rm -rf etc/apache2/sites-available/*default* opt/idpswitch/active

var/www/pyinfo.py

Lines changed: 0 additions & 5 deletions
This file was deleted.

var/www/sp/sp.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from html import escape
2+
from pathlib import Path
3+
4+
def status_page(titleword, environ, start_response):
5+
pieces = []
6+
def write(p):
7+
pieces.append(p + '\n')
8+
def menu(prefix, links):
9+
write('<p>' + prefix + ' | '.join(
10+
f'<a href="{escape(h)}">{escape(t)}</a>' for h, t in links))
11+
12+
host = environ['HTTP_HOST']
13+
hostfirst, _, hostrest = host.partition('.')
14+
title = f'{hostfirst} {titleword}, logged ' + ('IN' if environ.get('AUTH_TYPE') else 'out')
15+
16+
write('<!DOCTYPE html>')
17+
write('<meta charset="UTF-8">')
18+
write(f'<title>{escape(title)}</title>')
19+
write('<div style="font-size: 2em">')
20+
write(f'<h1 style="margin: auto">{escape(title)}</h1>')
21+
menu('here: ', [('/', 'public page'), ('/secret/', 'secret page')])
22+
if 'mellon' in host:
23+
menu('mellon: ', [
24+
('/mellon/login?ReturnTo=/', 'login'),
25+
('/mellon/logout?ReturnTo=/', 'logout'),
26+
('/mellon/invalidate?ReturnTo=/', 'invalidate'),
27+
])
28+
if 'shib' in host:
29+
menu('shib: ', [
30+
('/Shibboleth.sso/Login?target=/', 'login'),
31+
('/Shibboleth.sso/Logout?return=/', 'logout'),
32+
('/Shibboleth.sso/Status', 'status'),
33+
('/Shibboleth.sso/Session', 'session'),
34+
])
35+
sites = []
36+
for child in sorted(Path('/etc/apache2/sites-enabled').iterdir()):
37+
name = child.name.partition('.')[0]
38+
if 'default' in name: continue
39+
sites.append((f'https://{name}.{hostrest}/', name))
40+
if name == 'idp':
41+
sites.append((f'https://idp.{hostrest}/idp/', 'idp/idp'))
42+
sites.append((f'https://idp.{hostrest}/idp/profile/admin/hello', 'idp/...hello'))
43+
menu('sites: ', sites)
44+
45+
write('</div>')
46+
write('<h2>WSGI environment</h2>')
47+
write('<pre style="white-space: pre-wrap; word-break: break-all">{')
48+
for k, v in sorted(environ.items()):
49+
write(escape(f' {k!r}: {v!r},'))
50+
write('}</pre>')
51+
52+
start_response('200 OK', [('Content-Type', 'text/html;charset=UTF-8')])
53+
return [''.join(pieces).encode('utf-8')]
54+
55+
def application(environ, start_response):
56+
if environ['PATH_INFO'] == '/':
57+
return status_page('public', environ, start_response)
58+
if environ['PATH_INFO'] == '/secret/':
59+
return status_page('SECRET', environ, start_response)
60+
start_response('404 Not Found', [('Content-Type', 'text/html;charset=UTF-8')])
61+
return [b'404 Not Found']

var/www/spmellon/index.html

Lines changed: 0 additions & 15 deletions
This file was deleted.

var/www/spmellon/secret/index.html

Lines changed: 0 additions & 15 deletions
This file was deleted.

0 commit comments

Comments
 (0)