Categories
IdP Keycloak Linux SAML Shibboleth SingleSignOn SSO Ubuntu

SAML discovery service

Federated services

If you want to provide web bases services in a federated (SAML) environment, you’ll need a way for your users to select their home institution in order to log into the service using their local credentials.

That service is called discovery service.

What’s that discovery service doing?

So what happens if a user wants to access such a federated service:

  1. Some kind if dialog to select/search your home institution will be shown
  2. Once selected, the user will be redirected to the Identity Provider (IdP) of his institution
  3. After authenticating (according to the home institutions rules, e.g. including MFA) the user will be directed back to the originating service
  4. The Service Provider (SP) may do extra checks (using the transmitted SAML attributes) to check if the user is allowed to access the resource.
  5. If everything’s fine, the user can now use the service.

So next question: Where do I get such a discovery service?

Possible (ways to) discovery services

From a web developer perspective the simple answer may be: Write your own! As I’m more a system guy (and I don’t like to waste my time fiddling around with some ugly javascript libraries or css classes) I was looking for a ready to use solution.

The search proofed harder than expected: First thing I tried was the “Embedded Discovery Service (EDS)” of the Shibboleth software package. However I didn’t like the looks and the usability of it, so I looked for something else.

After quite some time I found thiss.io and even if it took me some time to figure how this thing is supposed to work in the end it matched my needs (and I think it also looks much better).

So here’s how to set up the discovery service including the metadata server:

Environment

Fortunately both required thiss projects provide a docker configuration for their components, so I used docker-compose to set them up:

services:

  thiss-js:
    image: thiss-js:latest
    container_name: thiss-js
    environment:
      - BASE_URL=https://ds.mydomain.de
      - STORAGE_DOMAIN=mydomain.de
      - MDQ_URL=https://mdq.mydomain.de/entities
      - SEARCH_URL=https://mdq.mydomain.de/entities
      - DEFAULT_CONTEXT=default
      - LOGLEVEL=warn
      - WHITELIST=mydomain.de
      - MIN_SEARCH_LEN=3
    ports:
      - 127.0.0.1:8088:80
    restart: unless-stopped

  # s. https://github.com/TheIdentitySelector/thiss-mdq/blob/master/docs/install.rst
  thiss-mdq:
    image: thiss-mdq:latest
    container_name: thiss-mdq
    volumes:
      - ./metadata.json:/etc/metadata.json
      - ./trustinfo.json:/etc/trustinfo.json
    environment:
      - BASE_URL=https://mdq.mydomain.de
    ports:
      - 127.0.0.1:8089:3000
    restart: unless-stopped

As you may have noticed the exported hosts are only available on localhost. That’s because I use an apache reverse proxy for SSL termination and to make the services available to the outside world.

While it is possible to run both the discovery service and the metadata query service on the same host I decided to go for two separate hosts (cause it makes the reverse proxy config a little easier).

Make sure to build both docker images (s. next chapters) before starting the docker containers using:

linux # docker compose up

The snippets from my apache config look like this (make sure to set up two reverse proxies: one for the discovery service (https://ds.mydomain.de) and one for the mdq service (https://mdq.mydomain.de):

<...>
Define BACKEND_HOST 127.0.0.1
Define BACKEND_PORT 8088
<...>
ProxyPass         /  http://${BACKEND_HOST}:${BACKEND_PORT}/
ProxyPassReverse  / http://${BACKEND_HOST}:${BACKEND_PORT}/
<...>

Discovery service

Building docker image:

linux # git clone https://github.com/TheIdentitySelector/thiss-js.git
linux # cd thiss-js
linux # make docker
linux # docker build --no-cache=true -t thiss-js:latest .

Metadata query service

Building docker image:

linux # git clone https://github.com/TheIdentitySelector/thiss-mdq.git
linux # cd thiss-mdq
linux # docker build --no-cache=true -t thiss-mdq:latest .

Configuration files needed: metadata.json (IdP metadata) and trustinfo.json (SP metadata).

For examples look at test/edugain.json (IdP metadata) or test/edugain-trustinfo.json (SP metadata).

If you want to add your own IDPs/SPs select a (short) one of the entries and adapt that to your setup. One thing that might get you into trouble however is a SHA1 hash sum contained in each metadata entry. According to the specification that checksum is of the entityID string. Here’s an easy way to calculate one:

linux # echo -n "https://keycloak.mydomain/realms/MYDOMAIN" | sha1sum 
f32c43411947eb965ab5173382e359825ec42f05  -

linux # cat metadata.json
[
  <...>
  {
    "title": "My Domain",
    "descr": "Identity Provider of Linux NG (LNG)",
    "title_langs": {
      "de": "My Domain",
      "en": "My Domain"
    },
    "descr_langs": {
      "de": "Identity Provider von My Domain",
      "en": "Identity Provider of My Domain"
    },
    "auth": "saml",
    "entity_id": "https://keycloak.mydomain.de/realms/MYDOMAIN",
    "entityID": "https://keycloak.mydomain.de/realms/MYDOMAIN",
    "type": "idp",
    "hidden": "false",
    "scope": "mydomain.de",
    "entity_icon_url": {
      "url": "https://www.mydomain.de/favicon.ico",
      "width": "16",
      "height": "16"
    },
    "privacy_statement_url": "https://www.mydomain.de/privacy",
    "id": "{sha1}f32c43411947eb965ab5173382e359825ec42f05"
  }
]

By standard an MDQ service implementation supports the following endpoints

linux # curl https://mdq.mydomain.de/entities | jq
[
  <... listing of all metadata here ...>
]
linux # curl 
https://mdq.mydomain.de/entities/%7Bsha1%7Df32c43411947eb965ab5173382e359825ec42f05
< ... returns metadata of sha1 matching entry ...>

(‘%7Bsha1%7D‘ means ‘{sha1}‘ URL encoded)

The thiss implementation however does add some extras (described here), like system status, text based searches or a listing of available entries (or their sha1 counterpart):

linux # curl https://mdq.mydomain.de/status
OK<no newline>
linux # curl https://mdq.mydomain.de/entities/?q=MYDOMAIN | jq 
[
  {
    "title": "My Domain",
    "descr": "Identity Provider of Linux NG (LNG)",
    "title_langs": {
      "de": "My Domain",
      "en": "My Domain"
    },
    "descr_langs": {
      "de": "Identity Provider von My Domain",
      "en": "Identity Provider of My Domain"
    },
    "auth": "saml",
    "entity_id": "https://keycloak.mydomain.de/realms/MYDOMAIN",
    "entityID": "https://keycloak.mydomain.de/realms/MYDOMAIN",
    "type": "idp",
    "hidden": "false",
    "scope": "mydomain.de",
    "entity_icon_url": {
      "url": "https://www.mydomain.de/favicon.ico",
      "width": "16",
      "height": "16"
    },
    "privacy_statement_url": "https://www.mydomain.de/privacy",
    "id": "{sha1}f32c43411947eb965ab5173382e359825ec42f05"
  }
]
linux # curl https://mdq.mydomain.de/.well-known/webfinger | jq
{
  [
    <... long list here ...>
    {
      "rel": "disco-json",
      "href": "https://mdq.mydomain.de/entities/{sha1}f32c43411947eb965ab5173382e359825ec42f05"
    }
  ]
}

How to create/convert json metadata

As you may have noticed creating the metadata by hand is a tedious task. Most federations provide their metadata in XML format only. So there should be a way to automate the conversion … and guess what: Yes there is: pyFF.

It uses some kind of pipeline to allow all kinds of modifications of metadata files like merging or converting to json. A simple sample config for doing that looks like this:

- load:
  - "/data/input.xml"
- select
- stats
- discojson
- publish:
    output: "/data/output.json"
    raw: true
    update_store: false

Calling pyff with this pipeline configuration converts a sample XML input file (/data/input.xml) to JSON format (/data/output.json) and it also gives some information (s. -stats) about the number and type of entries processed:

linux # pyff /data/xml2json.fd
---
total size:     1800
selected:       1800
          idps: 635
           sps: 1163
---

But pyFF cannot only be used as a conversion tool – it comes with its own MDQ server implementation with more options – so stay tuned for another post where we’ll replace thiss-mdq with pyFF!

Links

Leave a Reply

Your email address will not be published. Required fields are marked *