Adventures in Shibboleth and Nginx (Part 2 of 2)
The technicalities of part 1: how to build an Nginx + Shibboleth login system atop a RESTful API.
Welcome back! I’m going to assume that if you’re here now, you have already read Part 1. If not then do take a look if you’re interested in the higher level strucuture of our enghub.io room booking system, and how the following should look, as this article follows directly on as a guide.
The techniques I’ll talk about were learned from (and poured into) Roomie McRoomface, the code for which is available on our TechSoc Github.
Furthermore, all the configuration files I have described below are available in a repository on my Github account so I recommend you copy and paste from there rather than from the page as Medium messes up formatting pretty badly. This will also ensure you get the latest and most up to date versions of the files.
Requirements
This guide will allow you to build an authentication system with the following behaviour:
- Nginx web server on ports 80 and 443 handling all of your traffic;
- Shibboleth Service Provider (SP) backend configured to speak to a pre-configured Shibboleth Identity Provider (IdP);
- Some sort of Django / Node.js / Ruby backend that will host your code
(we used Django but, since I’m super nice, I’ll point out where something may differ between backends); - A live stream system to notify an independant front-end, perhaps written in React like us, when authentication has succeeded on the backend;
- Plenty of awesomeness :)
I will make the following assumptions about your setup (so please adjust accordingly!):
- Operating System: Ubuntu Server 64-bit, latest available. At the time of writing the two latest supported versions are 16.10 and 16.04 LTS.
- Shibboleth state: you have a working Identity Provider (IdP). This guide will not teach you how to set an IdP up. You should also have a working knowledge of how to set up a Shibboleth Service Provider (SP). If you are not an expert in this, do not worry, as I’ll cover in this guide the basics of setting this up. Again, however, this will be all from the perspective of the SP and will not teach you how to set your SP up in IdP; your system administrator should be able to do this for you.
(In other words, we set our SP up and beg and plead UCL to accept our Metadata then, after some black voodoo magic, our SP talks to IdP. We’re not sure how they do it, so I won’t try and teach you how they do it, either. Sorry!) - You should have full control over the server you are working on (e.g. a root shell is a prerequisite). I personally recommend spinning up a clean Azure or AWS instance for Shib work because you’ll need to install loads of things and sometimes it’s far easier just to start over cleanly.
- You have a way of setting up SSL. We use LetsEncrypt (because it’s free, open source and awesome!) but other methods are supported.
- You do not already have a web server installed. If you do, remove it, as we’ll be compiling Nginx from source.
- Your web app will expect to receive authentication at some callback endpoint. Don’t worry if you haven’t built this yet, as this guide should give you plenty of hints about how to create this.
Main Installation Guide: Let’s do it!
Firstly, we need to install some packages. You may wish to tweak this setup to suit which Nginx compilation options you plan to use.
# Ubuntu's version of Git is super old, so let's get the latest
sudo add-apt-repository ppa:git-core/ppa
sudo apt-get update# Fully upgrade all packages that are already installed. You might want to reboot after this if the kernel is upgraded...
sudo apt-get -y dist-upgradesudo apt-get install build-essential git libssl-dev supervisor shibboleth-sp2-common shibboleth-sp2-schemas shibboleth-sp2-utils libpcre3 libpcre3-dev wget
Once installed, we need to create some folders to ensure that all the services will start properly.
# Create socket folder for shibboleth
sudo mkdir /var/run/shibboleth# Create log folder for shibboleth
sudo mkdir /var/log/shibboleth
Assuming you managed to install Supervisor from the package repo as described above, you should be able to drop the following configuration file into /etc/supervisor/conf.d
with any name you like, so long as it ends in .conf; we used shib.conf
Supervisor is an awesome Python application that will ensure that the Shibboleth Authorizer and Responder services are set up with useful Unix sockets that Nginx will later be able to talk to. It will also allow you to start, stop and restart them from the command line.
[fcgi-program:shibauthorizer]
command=/usr/lib/x86_64-linux-gnu/shibboleth/shibauthorizer
socket=unix:///var/run/shibboleth/shibauthorizer.sock
socket_owner=_shibd:_shibd
socket_mode=0666
user=_shibd
stdout_logfile=/var/log/supervisor/shibauthorizer.log
stderr_logfile=/var/log/supervisor/shibauthorizer.error.log[fcgi-program:shibresponder]
command=/usr/lib/x86_64-linux-gnu/shibboleth/shibresponder
socket=unix:///var/run/shibboleth/shibresponder.sock
socket_owner=_shibd:_shibd
socket_mode=0666
user=_shibd
stdout_logfile=/var/log/supervisor/shibresponder.log
stderr_logfile=/var/log/supervisor/shibresponder.error.log
Grab it from: https://github.com/ChristopherHammond13/nginx-shibboleth-guide/blob/master/Supervisor/shib.conf
At this point, you should install your Shibboleth SP configuration to /etc/shibboleth
. The main files you’ll have to deal with are the following XML files:
- attribute-map.xml
This tells Shibboleth which attributes (bits of information) it can expect to receive from IdP, and what each bit of information is called. This should be provided to you by your administrator, and you should overwrite Shibboleth’s version with yours. - attribute-policy.xml
This maps certain data points to user groups. Again, your administrator should provide this to you and you should overwrite Shibboleth’s version with your own. We actually use the default version of this file at UCL, so it might be that your administrator will suggest that you do this too. - idp-ucl-metadata.xml (or similar)
In our setup, we have been provided with information by UCL on which certificates our Shibboleth SP should expect to receive data signed with. It lists our the Development, UAT (User Acceptance Testing) and Production IdPs, along with their relevant IDs. You should receive a similar XML file from your administrator that you can drop into this folder (and reference later). - protocols.xml
This sets up which authentication protocols (such as SAML versions) your servers should use. We use the out-of-box version of this file, and you should be able to as well, unless your administrator provides you with an alternative. - security-policy.xml
Like protocols.xml, the defaults are okay unless your admin says so. - shibboleth2.xml
This is the main XML file that I will go into a lot more detail on (as we’ll need to match some values up between here and the Nginx configuration).
In addition you’ll need a certificate and private key. These are often known as sp-cert.pem
and sp-key.pem
respectively, and should if possible be in the Shibboleth configuration directory. They do not need to be from an official Certificate Authority and you can easily generate them yourself using the shib-keygen
tool; just make sure that once you have generated these keys you do not change them, as the IdP will stop being able to talk to your SP.
shibboleth2.xml
Yes, it deserves its own section.
So, as mentioned, shibboleth2.xml is the main Shibboleth configuration file. Rather than teach you how to create a shibboleth2.xml file (which the Shibboleth Wiki is actually intended to do!) I have pasted our version of this file below, and highlighted in bold which sections need changing for your setup. Each bolded section is numbered (hey, look at me being organised!) so you can look at its explanation.
<SPConfig xmlns="urn:mace:shibboleth:2.0:native:sp:config"
xmlns:conf="urn:mace:shibboleth:2.0:native:sp:config"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
clockSkew="180"><!--
By default, in-memory StorageService, ReplayCache, ArtifactMap, and SessionCache
are used. See example-shibboleth2.xml for samples of explicitly configuring them.
--><!--
To customize behavior for specific resources on Apache, and to link vhosts or
resources to ApplicationOverride settings below, use web server options/commands.
See https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPConfigurationElements for help.For examples with the RequestMap XML syntax instead, see the example-shibboleth2.xml
file, and the https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPRequestMapHowTo topic.
--><!-- BEGIN BOLDED SECTION 1 --> <RequestMapper type="XML">
<RequestMap>
<Host name="enghub.io"
authType="shibboleth"
requireSession="true"
redirectToSSL="443">
<Path name="secure" />
<Path name="api/v1/user.login.callback" />
</Host>
</RequestMap>
</RequestMapper><!-- END BOLDED SECTION 1 --><!-- The ApplicationDefaults element is where most of Shibboleth's SAML bits are defined. --><!-- BEGIN BOLDED SECTION 2 -->
<ApplicationDefaults entityID="https://enghub.io/shibboleth"
REMOTE_USER="eppn persistent-id targeted-id">
<!-- END BOLDED SECTION 2 --><!--
Controls session lifetimes, address checks, cookie handling, and the protocol handlers.
You MUST supply an effectively unique handlerURL value for each of your applications.
The value defaults to /Shibboleth.sso, and should be a relative path, with the SP computing
a relative value based on the virtual host. Using handlerSSL="true", the default, will force
the protocol to be https. You should also set cookieProps to "https" for SSL-only sites.
Note that while we default checkAddress to "false", this has a negative impact on the
security of your site. Stealing sessions via cookie theft is much easier with this disabled.
-->
<Sessions lifetime="28800" timeout="3600" relayState="ss:mem"
checkAddress="false" handlerSSL="true" cookieProps="https"><!--
Configures SSO for a default IdP. To allow for >1 IdP, remove
entityID property and adjust discoveryURL to point to discovery service.
(Set discoveryProtocol to "WAYF" for legacy Shibboleth WAYF support.)
You can also override entityID on /Login query string, or in RequestMap/htaccess.
--><!-- BEGIN BOLDED SECTION 3 --> <SSO entityID="https://shib-uat-idp.ucl.ac.uk/shibboleth">
SAML2 SAML1
</SSO><!-- END BOLDED SECTION 3 --><!-- SAML and local-only logout. -->
<Logout>SAML2 Local</Logout><!-- Extension service that generates "approximate" metadata based on SP configuration. -->
<Handler type="MetadataGenerator" Location="/Metadata" signing="true"/><!-- Status reporting service. -->
<Handler type="Status" Location="/Status" acl="127.0.0.1 ::1"/><!-- Session diagnostic service. -->
<Handler type="Session" Location="/Session" showAttributeValues="true"/><!-- JSON feed of discovery information. -->
<Handler type="DiscoveryFeed" Location="/DiscoFeed"/>
</Sessions><!--
Allows overriding of error template information/filenames. You can
also add attributes with values that can be plugged into the templates.
-->
<Errors supportContact="root@localhost"
helpLocation="/about.html"
styleSheet="/shibboleth-sp/main.css"/><!-- Example of remotely supplied batch of signed metadata. -->
<!--
<MetadataProvider type="XML" uri="http://federation.org/federation-metadata.xml"
backingFilePath="federation-metadata.xml" reloadInterval="7200">
<MetadataFilter type="RequireValidUntil" maxValidityInterval="2419200"/>
<MetadataFilter type="Signature" certificate="fedsigner.pem"/>
</MetadataProvider>
--><!-- BEGIN BOLDED SECTION 4 --> <!-- This SP is UCL only -->
<MetadataProvider type="XML" file="idp-ucl-metadata.xml" legacyOrgNames="true"/><!-- END BOLDED SECTION 4 --><!-- Example of locally maintained metadata. -->
<!--
<MetadataProvider type="XML" file="partner-metadata.xml"/>
--><!-- Map to extract attributes from SAML assertions. -->
<AttributeExtractor type="XML" validate="true" path="attribute-map.xml"/><!-- Use a SAML query if no attributes are supplied during SSO. -->
<AttributeResolver type="Query" subjectMatch="true"/><!-- Default filtering policy for recognized attributes, lets other data pass. -->
<AttributeFilter type="XML" validate="true" path="attribute-policy.xml"/><!-- BEGIN BOLDED SECTION 5 --><!-- Simple file-based resolver for using a single keypair. -->
<CredentialResolver type="File" key="sp-key.pem" certificate="sp-cert.pem"/><!-- END BOLDED SECTION 5 --><!--
The default settings can be overridden by creating ApplicationOverride elements (see
the https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPApplicationOverride topic).
Resource requests are mapped by web server commands, or the RequestMapper, to an
applicationId setting.Example of a second application (for a second vhost) that has a different entityID.
Resources on the vhost would map to an applicationId of "admin":
-->
<!--
<ApplicationOverride id="admin" entityID="https://admin.example.org/shibboleth"/>
--></ApplicationDefaults><!-- Policies that determine how to process and authenticate runtime messages. -->
<SecurityPolicyProvider type="XML" validate="true" path="security-policy.xml"/><!-- Low-level configuration about protocols and bindings available for use. -->
<ProtocolProvider type="XML" validate="true" reloadChanges="false" path="protocols.xml"/>
</SPConfig>
Grab a much neater version of this from: https://github.com/ChristopherHammond13/nginx-shibboleth-guide/blob/master/Shibboleth/shibboleth2.xml
Bolded Section 1: The Request Mapper
The request mapper is basically an instruction to Shibboleth to respond to only certain requests to certain endpoints. Change enghub.io
to whatever your domain name is (as we got there first with that domain!), change the path name to something more inventive than secure
and then set the long value to whichever endpoint you would expect Shibboleth to redirect to. This does not have to exist yet.
Note that you can change and update your request mapper however much you like even after going live (e.g. if you add a new callback or update your current one). Just restart shibd
any time you do.
Bolded Section 2: Your Entity ID
This is a URI that identifies your Service Provider. It does not have to resolve to anything, and in fact it probably shouldn’t. The recommended default is to use https://yourdomainhere.tld/shibboleth
as shown in the example.
Bolded Section 3: IdP’s Entity ID
This is the entity ID (like section 2) of the Identity Provider you would like to connect to. Note again that this doesn’t have to resolve; that is up to the file referenced in section 4. In this case we are connecting to the UAT IdP at UCL, but the live version of our code uses the production IdP.
Bolded Section 4: Metadata Provider
This connects up an XML file that describes loads more detail about IdP. It maps Entity IDs to actual URLs, and also provides public keys to match against. You should change idp-ucl-metadata.xml
to the filename of your institution’s XML file.
Bolded Section 5: Credential Resolver
This is where you enter the paths to sp-cert.pem
and sp-key.pem
, which were mentioned above. They uniquely identify and authorise your SP, so keep this private key private!
Moving Swiftly On
Now that we have dealt with the abomination that is the configuration of Shibboleth, the next stage is to download, compile and customise Nginx. This will involve integrating a few plugins which I will explain the purposes of.
# Set the Nginx version we want to work with. At the time of writing the latest is 1.11.8. You can find out from https://nginx.org which version is the latest so that you can update this variable.
NGINX_VERSION=1.11.8# Download and extract the Nginx source
wget -O - "https://nginx.org/download/nginx-$NGINX_VERSION.tar.gz" | tar zxvf -# Clone the Nginx push stream module
git clone https://github.com/wandenberg/nginx-push-stream-module.git push-stream# Clone the Nginx Shibboleth Module
git clone https://github.com/nginx-shib/nginx-http-shibboleth.git http-shib# Clone the Clear Headers module
git clone https://github.com/openresty/headers-more-nginx-module.git headers-more# Enter the Nginx folder
pushd nginx-$NGINX_VERSION# Configure the Nginx compilation
./configure --with-http_ssl_module \
--add-module=../push-stream --add-module=../http-shib \
--add-module=../headers-more# Compile Nginx
make# Install Nginx
make install# Back to the home folder
popd
The three modules we have added are:
- Nginx HTTP Push Stream Module
This will allow us to push those log in notifications to our client from the server. - Nginx HTTP Shibboleth Module
This allows Nginx to integrate with the Shibboleth Authorizer and Responder. - Nginx HTTP Clear Headers Module
This is recommended by the creators of the Shibboleth module, as it helps to avoid header manipulation attacks that could compromise the security of your application.
Once Nginx is installed, the final stage is to configure it to speak to Shibboleth. Head to the /usr/local/nginx/conf
folder and wipe out the contents. Paste the following files in:
fastcgi.conf
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param DOCUMENT_URI $document_uri;
fastcgi_param DOCUMENT_ROOT $document_root;
fastcgi_param SERVER_PROTOCOL $server_protocol;
fastcgi_param REQUEST_SCHEME $scheme;
fastcgi_param HTTPS $https if_not_empty;fastcgi_param GATEWAY_INTERFACE CGI/1.1;
fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;fastcgi_param REMOTE_ADDR $remote_addr;
fastcgi_param REMOTE_PORT $remote_port;
fastcgi_param SERVER_ADDR $server_addr;
fastcgi_param SERVER_PORT $server_port;
fastcgi_param SERVER_NAME $server_name;# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param REDIRECT_STATUS 200;
Grab it from: https://github.com/ChristopherHammond13/nginx-shibboleth-guide/blob/master/Nginx/fastcgi.conf
fastcgi_params
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param DOCUMENT_URI $document_uri;
fastcgi_param DOCUMENT_ROOT $document_root;
fastcgi_param SERVER_PROTOCOL $server_protocol;
fastcgi_param REQUEST_SCHEME $scheme;
fastcgi_param HTTPS $https if_not_empty;fastcgi_param GATEWAY_INTERFACE CGI/1.1;
fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;fastcgi_param REMOTE_ADDR $remote_addr;
fastcgi_param REMOTE_PORT $remote_port;
fastcgi_param SERVER_ADDR $server_addr;
fastcgi_param SERVER_PORT $server_port;
fastcgi_param SERVER_NAME $server_name;# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param REDIRECT_STATUS 200;
Grab it from: https://github.com/ChristopherHammond13/nginx-shibboleth-guide/blob/master/Nginx/fastcgi_params
nginx.conf
I have highlighted and annotated in bold what needs to be changed.
user www-data;worker_processes 1;events {
worker_connections 1024;
}http {
include mime.types;
default_type application/octet-stream;log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';access_log logs/access.log main;sendfile on;
keepalive_timeout 65;
# You may wish to change this if you have memory errors when sending a streamed notification
push_stream_shared_memory_size 2M;# Redirect HTTP connections to HTTPS
server {
listen 80;
server_name _;
return 301 https://$host$request_uri;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}# HTTPS serverserver {
listen 443 ssl;# Change this to your server's domain name
server_name enghub.io;# Update these paths to your actual certificate files.
ssl_certificate /path/to/enghub.io/fullchain.pem;
ssl_certificate_key /path/to/enghub.io/privkey.pem;ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;\# Set this to your catch all where all your static site content will be. We have a static frontend (React) and a dynamic backend (Django) so this makes sense to us. If not, adjust accordingly.
root /path/to/static/site;location = /shibauthorizer {
internal;
include fastcgi_params;
fastcgi_pass unix:/var/run/shibboleth/shibauthorizer.sock;
}location /Shibboleth.sso {
include fastcgi_params;
fastcgi_pass unix:/var/run/shibboleth/shibresponder.sock;
}location /shibboleth-sp {
alias /usr/share/shibboleth/;
}# Change this to your desired push publish endpoint
location ~ /api/v1/push.publish {
push_stream_publisher admin;
push_stream_channels_path $arg_id;
push_stream_store_messages off;
}# Change this to your desired socket subscribe endpoint
location ~ /api/v1/push.subscribe/(.*) {
push_stream_subscriber;
push_stream_channels_path $1;
}# Change this to your desired long poll subscription endpoint
location ~ /api/v1/push.subscribe_longpoll/(.*) {
push_stream_subscriber long-polling;
push_stream_channels_path $1;
push_stream_message_template "{\"id\":~id~,\"channel\":\"~channel~\",\"text\":\"~text~\"}";
push_stream_longpolling_connection_ttl 30s;
push_stream_last_received_message_time "$arg_time";
push_stream_last_received_message_tag "$arg_tag";
}# Change this to your callback code for when a user has logged into Shibboleth
location ~ /api/v1/user.login.callback {
include shib_clear_headers;
shib_request_use_headers on;
shib_request /shibauthorizer;
# Change this to the URL of or socket for your web application
proxy_pass http://localhost:8000;
}# Change this to the general URL or socket for your application if the user is not visiting a Shibboleth callback URL
location /api {
proxy_pass http://localhost:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}
Grab a much neater and better annotated version from: https://github.com/ChristopherHammond13/nginx-shibboleth-guide/blob/master/Nginx/nginx.conf
Note above that I separated out the callback endpoint from the general API. This is for efficiency purposes, as Shibboleth’s Authorizer backend should only need calling to pass authentication details from IdP to the application. For all other endpoints there is no need to touch Shibboleth, so it saves a bunch of background processing for every other endpoint.
shib_fastcgi_params
This file actually comes bundled with the Shib Nginx plugin but I was getting crashes in Nginx without tweaking it, so the version I have that works is below. Your mileage may vary. Again, I have bolded important parts of this.
# vim: set filetype=conf :# My edit is below. I had issues when I did not include fastcgi_params in this file.
include fastcgi_params;# Replace `fastcgi_param` with `sgci_param`, `uwsgi_param` or similar
# directive for use with different upstreams. Consult the relevant upstream
# documentation for more information on environment parameters.
#
# Auth-Type is configured as authType in
# https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPContentSettings.
# Other default SP variables are as per
# https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPAttributeAccess#NativeSPAttributeAccess-CustomSPVariablesshib_request_set $shib_auth_type $upstream_http_variable_auth_type;
fastcgi_param Auth-Type $shib_auth_type;shib_request_set $shib_shib_application_id $upstream_http_variable_shib_application_id;
fastcgi_param Shib-Application-ID $shib_shib_application_id;shib_request_set $shib_shib_authentication_instant $upstream_http_variable_shib_authentication_instant;
fastcgi_param Shib-Authentication-Instant $shib_shib_authentication_instant;shib_request_set $shib_shib_authentication_method $upstream_http_variable_shib_authentication_method;
fastcgi_param Shib-Authentication-Method $shib_shib_authentication_method;shib_request_set $shib_shib_authncontext_class $upstream_http_variable_shib_authncontext_class;
fastcgi_param Shib-AuthnContext-Class $shib_shib_authncontext_class;shib_request_set $shib_shib_authncontext_decl $upstream_http_variable_shib_authncontext_decl;
fastcgi_param Shib-AuthnContext-Decl $shib_shib_authncontext_decl;shib_request_set $shib_shib_identity_provider $upstream_http_variable_shib_identity_provider;
fastcgi_param Shib-Identity-Provider $shib_shib_identity_provider;shib_request_set $shib_shib_session_id $upstream_http_variable_shib_session_id;
fastcgi_param Shib-Session-ID $shib_shib_session_id;shib_request_set $shib_shib_session_index $upstream_http_variable_shib_session_index;
fastcgi_param Shib-Session-Index $shib_shib_session_index;shib_request_set $shib_remote_user $upstream_http_variable_remote_user;
fastcgi_param Remote-User $shib_remote_user;# Uncomment any of the following core attributes. Consult your Shibboleth
# Service Provider (SP) attribute-map.xml file for details about attribute
# IDs. Add additional directives for any Shibboleth attributes released to
# your SP.shib_request_set $shib_eppn $upstream_http_variable_eppn;
fastcgi_param eppn $shib_eppn;# shib_request_set $shib_affliation $upstream_http_variable_affiliation;
# fastcgi_param Affiliation $shib_affiliation;# shib_request_set $shib_unscoped_affliation $upstream_http_variable_unscoped_affiliation;
# fastcgi_param Unscoped-Affiliation $shib_unscoped_affiliation;shib_request_set $shib_entitlement $upstream_http_variable_entitlement;
fastcgi_param Entitlement $shib_entitlement;shib_request_set $shib_targeted_id $upstream_http_variable_targeted_id;
fastcgi_param Targeted-Id $shib_targeted_id;shib_request_set $shib_persistent_id $upstream_http_variable_persistent_id;
fastcgi_param Persistent-Id $shib_persistent_id;shib_request_set $shib_transient_name $upstream_http_variable_transient_name;
fastcgi_param Transient-Name $shib_transient_name;shib_request_set $shib_commonname $upstream_http_variable_commonname;
fastcgi_param Commonname $shib_commonname;shib_request_set $shib_displayname $upstream_http_variable_displayname;
fastcgi_param DisplayName $shib_displayname;shib_request_set $shib_email $upstream_http_variable_email;
fastcgi_param Email $shib_email;shib_request_set $shib_organizationname $upstream_http_variable_organizationname;
fastcgi_param OrganizationName $shib_organizationname;
Grab it from: https://github.com/ChristopherHammond13/nginx-shibboleth-guide/blob/master/Nginx/shib_fastcgi_params
If there are other attributes defined in Shibboleth that you would like to gain access to, you should set them up in this file, too. Follow this pattern:
shib_request_set $shib_attributenamehere $upstream_http_variable_attributenamehere;
fastcgi_param AttributeNameHere $shib_AttributeNameHere;
shib_clear_headers
The final main configuration file is for the Clear Headers module. This is tweaked from the original provided by the Shibboleth Nginx module.
# Ensure that you add directives to clear input headers for *all* attributes
# that your backend application uses. This may also include variations on these
# headers, such as differing capitalisations and replacing hyphens with
# underscores etc -- it all depends on what your application is reading.
#
# Note that Nginx silently drops headers with underscores
# unless the non-default `underscores_in_headers` is enabled.# Shib-* doesn't currently work because * isn't (yet) supported
more_clear_input_headers
Auth-Type
Shib-Application-Id
Shib-Authentication-Instant
Shib-Authentication-Method
Shib-Authncontext-Class
Shib-Identity-Provider
Shib-Session-Id
Shib-Session-Index
Remote-User;more_clear_input_headers
EPPN
Affiliation
cn
Unscoped-Affiliation
Entitlement
Targeted-Id
Persistent-Id
Transient-Name
Commonname
DisplayName
OrganizationName;
Grab it from: https://github.com/ChristopherHammond13/nginx-shibboleth-guide/blob/master/Nginx/shib_clear_headers
Again, this should be updated to support your configuration. You should add new attribute names to the second list after OrganisationName
such that they match up with the shib_fastcgi_params
file above.
Coming Online!
Now that most things are configured, we can bring Shibboleth and Nginx online.
sudo /etc/init.d/shibd restart
sudo supervisorctl restart all
sudo /usr/local/nginx/sbin/nginx
If all is well then those three commands should work well. The only stage left for configuration is to ensure that Nginx runs at boot. Even though it’s possible to run the command directly as shown above, it’s possibly easier to just create an Upstart script. There are instructions available on the Nginx website.
Time to Code
Now that everything is configured, I will explain how we programmatically approached the problem. To recap from the previous article, we wanted to be able to handle authentication away from the app itself and not redirect the user away from the application if possible. Instead, we open the Shib login window in a new tab, redirect that tab back to a callback URL then that endpoint creates the user account internally (if necessary), sets up the session and sends an API key to the frontend. A more formal workflow for this, from the perspective of the frontend, is as follows:
- Request a login URL from the backend containing a randomly generated login token. This token can be any length you like, but I recommend you stick to letters and numbers. I also recommend it’s pretty long because that token will be the name of the channel on which the API token will be sent on; therefore, we do not want the ID to be easily guessable. Our tokens are “shib” followed by sixty random characters so that it is not feasible to try and guess them.
- Start listening on a push channel with the ID set to the Shibboleth login token generated above.
- Open up the Shibboleth login URL sent in Step #1 in a new window/tab.
- Once the user logs in they’ll be sent to the callback URL which will take the Shib data, process it (e.g. create database entries if necessary) and generate an API token.
- The API token should then be sent to the frontend via the push stream chaannel associated with the Shib token.
- The frontend should then close the window/tab and continue the login process with the API token. This token should provide access to all the user data the callback obtained from IdP.
I realise this is quite convoluted (but it works surprisingly well) so I will explain each step with some code examples.
Getting a Login URL and Shibboleth Token
Shibboleth Login URLs should always contain at least one GET parameter: the target. This is the callback URL, and can be set to anything you, as the programmer, decide. In this case, we embed the aforementioned randomly generated shib token as a GET parameter to the callback, which is then in turn a URL Encoded target parameter. This looks something like this:
https://yourdomain.tld/Shibboleth.SSO/Login?target=https%3A%2F%2Fyourdomain.tld%2Fapi%2Fuser%2Flogin.callback%3Ftoken%3Dshibabcdefghijklmnopqrstuvwxyz
The URL Encoding here doesn’t help explain what’s going on, so here’s a version without that:
https://yourdomain.tld/Shibboleth.SSO/Login?target=https://yourdomain.tld/api/user/login.callback?token=shibabcdefghijklmnopqrstuvwxyz
What we’ve done here is generated a token with the ID shibabcdefghijklmnopqrstuvwxyz
and told the login URL to append that token to the callback URL as a parameter. That way, once login has succeeded, the callback will know which client frontend was trying to login and hence know where to send the API key to.
This generated Login URL should be sent via a JSON API request from the frontend.
Listening on the Push Channel
In order to detect when login has completed in the other tab, the client should listen on the channel specified in the Shibboleth response. The code below should help you get started with this, but note that it requires the JavaScript Client for the Push Stream Library, as well as jQuery. The docs for this library are available here.
var api_key;
function messageReceived(text, id, channel)
{
var data = JSON.parse(window.atob(text));
api_key = data["api_key"];
}var pushstream = new PushStream
(
{
host: window.location.hostname,
port: window.location.port,
modes: "longpolling",
tagArgument: 'tag',
timeArgument: 'time',
timeout: 30000,
messagesPublishedAfter: 5,
urlPrefixLongpolling: '/api/v1/push.subscribe_longpoll'
}
);pushstream.onmessage = messageReceived;
pushstream.addChannel(shibboleth_login_token_id);
pushstream.connect();
Grab it from: https://github.com/ChristopherHammond13/nginx-shibboleth-guide/blob/master/Stream-Client-Sample/stream-client-sample.js
This is the point where I make a strong recommendation for your backend (which I’ll talk in more detail about later): you should base-64 encode the JSON data you send from your backend to your frontend. The reason for this is that the push stream module itself sends JSON data to the client, and will try and slot your JSON data into a string object called text. As soon as your send any quotation marks of your own you’ll trip up the JSON response making it invalid, so base-64 encoding the data makes this happen smoothly. The reason we used window.atob
above is that the code is programmed to expect base-64 within the JSON.
To see how this is implemented in React, take a look at our Roomie McRoomface frontend code.
Getting the User to Login
Just open up a popup window using JavaScript to the Shibboleth Login URL your backend sends the frontend via an API call. Nice and easy!
The Callback Endpoint (Backend)
This is the final complex part of this setup. At this point I have covered how the frontend will present a login page to the user and how it’ll listen for a response, but now we need to create the callback function itself. The code we used for this in Django is as follows; I’ll bold the important parts and explain what’s going on. The full code for this file is available on our GitHub.
def login_callback(request):
try:
sid = request.GET['sid'] # We used 'sid' to refer to our Shibboleth token ID
except:
return HttpResponse('No sid supplied, so login cannot continue.')try:
# Check that the token is valid first (e.g. it was requested by the front end at some point) by reading it from the login tokens table in the database
sid_data = ShibLoginToken.objects.get(sid=sid)
except ShibLoginToken.DoesNotExist:
return HttpResponse('Invalid sid. Please try logging in again.')# As this is the callback, the Shibboleth attributes will be added to the HTTP request headers. We can get this header data in Python using the following code. Note that all text is set to upper case and all formatting from the attribute name is removed. The field name is then set to HTTP_ATTRIBUTENAMEHERE.
try:
eppn = request.META['HTTP_EPPN']
groups = request.META['HTTP_UCLINTRANETGROUPS']
cn = request.META['HTTP_CN']
department = request.META['HTTP_DEPARTMENT']
given_name = request.META['HTTP_GIVENNAME']
surname = request.META['HTTP_SN']
except:
return HttpResponse(
'No Shibboleth data. This page should not be accessed directly!')# check if the user is in the internal whitelist
white_listed = WhiteList.objects.filter(eppn=eppn).exists()# Groups is a custom Shibboleth attribute UCL adds to requests so that we can work out which type of user has logged in. We also have a manual username white list to override this.
if "engscifac-ug" not in groups.split(';') and not white_listed:
login_response = {
"result": "failure",
"message": ("This system is available only"
" to members of the engineering faculty.")
}# Create a user account for somebody in the system if they have never logged in before. This allows us to tie an API key to a user account, and also display any user data we want in the UI.
else:
if User.objects.filter(email=eppn).exists():
user = User.objects.get(email=eppn)
else:
User.objects.create_user(
username=cn,
email=eppn,
password=utils.random_string(128),
first_name=given_name,
last_name=surname
)
user = User.objects.get(email=eppn)
group_2 = Group.objects.get(name="Group_2")
user.groups.add(group_2)
user.save()
up = UserProfile(user=user)
up.department = department
up.save()# Create a new login token for the user who just logged in
token, created = Token.objects.get_or_create(user=user)
token.save()# The JSON response to send back to the client via the push stream channel
login_response = {
"result": "success",
"message": "Login successful",
"email": user.email,
"quota_left": user.user_profile.quota_left,
'token': token.key,
"societies": [
[k.user.first_name, k.user.username] for k in
user.user_profile.associated_society.all()
],
"groups": [k.name for k in user.groups.all()]
}# Delete the Shibboleth login token so that it can be used again if randomly generated in the future.
try:
t = ShibLoginToken.objects.get(user=user)
if t.sid != sid:
t.delete()
except ShibLoginToken.DoesNotExist:
print("User has never tried logging in before, so there was nothing to delete. Continuing...")try:
token = ShibLoginToken.objects.get(sid=sid)
token.status = 1
token.user = user
token.save()
except Exception as e:
print("Error updating token in database")
print(e)url = STREAM_PUBLISH_URL + "/?id=" + sid# Dump the login data generated above to a JSON string then base 64 encode it ready to be sent to the frontend.
# Then post this base-64 data to the push stream module on the channel ID with the same name as the Shib login token.
try:
login_response_str = json.dumps(login_response)
b64 = base64.b64encode(login_response_str.encode('utf-8'))
r = requests.post(url, data=b64)
print(r.text)
except Exception as e:
print("Error sending the data to stream backend")
print(e)# Write the login data to the page as a response (useful for testing, but in theory as soon as this data is sent the popup window should be closed off by the frontend so the user should not, in theory, ever even see this)
response = HttpResponse(content_type="text/html")
response.write(login_response)
return response
Medium is terrible with tabs and spacing, so grab a properly clean and indented version from: https://github.com/ChristopherHammond13/nginx-shibboleth-guide/blob/master/Django-Backend-Sample/backend-sample-view.py
Just a little aside here: an EPPN is known internally in Shibboleth as an eduPerson-principleName, or in other words represents a unique identifier for a user in the IdP. At UCL the EPPN is our unique_seven_letter_username@ucl.ac.uk, but this could be totally different in your institution.
All Done!
Hopefully you found this guide helpful and/or interesting, and do let me know if you need anything else clarifying.
-Chris