SAML2jwt får din tjeneste på WAYF med få linjer kode

Enkel teknisk integration af tjeneste i WAYF, med JWT

Hvorfor?

Erfaringsmæssigt kan det være en stor mundfuld for udviklere at skulle arbejde med SAML2-protokollen og XML — hvilket hidtil har været nødvendigt når en tjeneste skal kunne modtage logins fra WAYF. Noget mere overkommeligt er det typisk at arbejde med JSON og JSON Web Tokens (JWT) — som mange udviklere kender fx fra OAuth eller måske OIDC. Derfor har WAYF nu udviklet og idriftsat et interface som gør det muligt at integrere en webtjeneste med WAYF via 25-75 kodelinjers JWT-håndtering — uden XML eller SAML2. Interfacet er en mikroservice som vi kalder SAML2jwt. Idéen er at tjenesten ganske vist sender og modtager SAML2-protokoltrafik i XML — men via kald af SAML2jwt før afsendelse hhv. efter modtagelse får WAYF til at oversætte al relevant information til en let-håndtérbar JWT. Eneste WAYF-relaterede afhængighed i tjenestens programkode bliver derfor et selvvalgt JWT-bibliotek (inkl. kryptofunktioner til validering af JWT-signaturer).

Hvordan?

Når en webtjeneste skal have et bruger-login fra WAYF med SAML2jwt, er flow'et sådan her:

  1. Tjenesten modtager en loginanmodning fra brugerens browser.
  2. Tjenesten kalder https://wayf.wayf.dk/saml2jwt (server2server) med HTTP-GET med parametrene issuer og acs tilføjet. Værdien af issuer sættes til tjenestens protokolnavn hos WAYF (en aftalt URI), værdien af acs til den URL som tjenesten senere skal modtage login-svaret på.
  3. Tjenesten får et HTTP-svar fra SAML2jwt og sender headerne fra herfra videre til browseren. Vær hér opmærksom på at bruge et HTTP-bibliotek som ikke følger redirects automatisk.
  4. Tjenesten modtager en HTTP-POST fra browseren med det rå login-svar fra WAYF.
  5. Tjenesten POST'er indholdet af den modtagne POST til https://wayf.wayf.dk/saml2jwt (server2server) med parametrene issuer og acs tilføjet med samme værdier som i det oprindelige GET-kald ovenfor.
  6. Tjenesten modtager en JWT som svar, opsplitter den i sine tre bestanddele (hoved, krop og signatur) og verificerer signaturen med WAYFs offentlige nøgle og den aftalte signeringsalgoritme.
  7. Tjenesten behandler brugeroplysningerne i JSON-token'en (JWT'ens krop) ifølge sin forretningslogik. Den autentificerede brugers attributter indgår i JWT'en med deres almindelige skemanavne som nøgler (fx 'eduPersonPrincipalName' eller '1.3.6.1.4.1.5923.1.1.1.6')

Tjenesten skal være kendt af WAYF (dvs. registreret i WAYFs metadataregister) på helt samme måde som almindeligt integrerede tjenester.

Eksempel

Herunder ses et stykke PHP-kode svarende til hvad man kan nøjes med i sin applikation hvis den får logins fra WAYF med brug af JWT og SAML2jwt:

<?php
$publicKey = // prod key
'-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA54K3rUT473T2Ot5VRXW0UdB72beHrXZbfw5q5Z+1VWvAfQImlgIUYAxQFwMWQ+ChwD5ekHapp0X792cSsgNtNWDGrJ7AHRM5aYyioySeuSZLTHQCJEcgG2TMhYVqb4TAa0UYDkfDeyMY+gNeDhwZPvfW6gKS2wfkQu354UNdIE5SDHIrNl/w1NdFsuSwh0/E2BnTh7klrAWjVydhtNVhByV4yo5hjesgNfEDwFObhlgI2TEs7S/tgrv6Z8XlPVDkLsVeK+hAj4DaaL+oaXAu36gPzWN1iSL+/sCldmLSx1xGi8D0fjIUK/i1Pl/8+W7xgnxpdIaknyABM+2Rj26X1wIDAQAB
-----END PUBLIC KEY-----';

$saml2jwt       = 'https://wayf.wayf.dk/saml2jwt';
$idplist        = []; //['idp.entity.test'];

$params = ['acs'    =>  'http://sp.entity.test:8080/sp.php',
           'issuer' =>  'sp.entity.test'];

$opts = ['http'=> ['follow_location' => 0,
                  'method'  => 'POST',
                  'header'  => "Cookie: wayfid=wayf-qa;\r\n" .
                               "Content-Type: application/x-www-form-urlencoded",],
         'ssl' => ['verify_peer' => false, 'verify_peer_name' => false]];

if (empty($_POST['SAMLResponse'])) {
  $params['idplist'] = join(',', $idplist);
  $opts['http']['content'] = http_build_query($params);
  file_get_contents($saml2jwt, false, stream_context_create($opts));
  array_walk($http_response_header, function($header) { header($header, false); });
} else {
  $opts['http']['content'] = http_build_query(array_merge($params, $_POST));
  $jwt =  file_get_contents($saml2jwt, false, stream_context_create($opts));
  list($header, $body, $signature) = explode(".", $jwt);
  $ok = openssl_verify("$header.$body", base64url_decode($signature), $publicKey, 'sha256');
  $payload =  $ok === 1 ? json_decode(base64url_decode($body), true) : [];

  header('content-type: text/plain');
  foreach(['iat', 'nbf', 'exp'] as $k ) {
      $payload[$k] = date(DATE_ISO8601, $payload[$k]);
  }
  print_r($payload);
}

function base64url_decode($b64url) { return base64_decode(strtr($b64url, ['-' => '+', '_' => '/'])); }

Nemt at teste

Som tjenesteudvikler kan man nemt teste SAML2jwt — sådan her:

  1. Gem PHP-koden ovenfor i en fil sp.php.
  2. Start PHP's indbyggede webserver med kommandoen 'php -S 0.0.0.0:8080' i den mappe hvor du har gemt sp.php.
  3. Sæt i din maskines hosts-fil DNS-navnet sp.entity.test til at pege på den “server” hvor du vil køre sp.php — fx din egen PC, hvis det er dén du har udført ovenstående på.
  4. Gå til http://sp.entity.test:8080/sp.php i en browser. Med et SAML-plugin (i FireFox fx SAML Tracer) kan du nemt følge login-flow'et i browseren.

Testen herover er automatisk i den forstand at den starter et login-flow når man “kalder” http://sp.entity.test:8080/sp.php, og simpelthen dump'er den JWT som flow'et slutter med. Du skal regne med en advarsel om skift fra HTTPS til HTTP sidst i flow'et.

Testen bruger WAYFs testmiljø og kan kun gennemføres hvis man har en brugerkonto i “institutionen” WAYF Orphanage. Hvis man ikke har sådan en, skal man anmode om en på https://orphanage.wayf.dk og afvente WAYF-sekretariatets godkendelse.

Hent klientkode på GitHub

På GitHub kan man hente klientkode til PHP (samme som herover) og til C#.