title: Cannot make passport-saml work with ADFS
body: <p>I have implemented SSO with ADFS on a NodeJS application using express, passport and passport-saml. I can login on ADFS and I'm correctly posted back to my callback route (which is localhost:3000/adfs/postResponse) together with a SAML-token. However, when I reach the callback route, it seems like the SAML-token is rejected so I'm sent back to login on ADFS. This is then repeated. </p>
  
  <p>Can anyone suggest what could be wrong? Any help will be appreciated. </p>
  
  <p>More details: </p>
  
  <p>This SAML request is sent to ADFS: </p>
  
  <pre><code>&lt;?xml version="1.0"?&gt;
  &lt;samlp:AuthnRequest AssertionConsumerServiceURL="https://localhost:3000/adfs/postResponse"
      Destination="https://nonp-adfs.dsgapps.dk/adfs/ls" ID="_5e09625f5c3f5dbb0b6b"
      IssueInstant="2018-06-28T11:57:35.962Z"
      ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Version="2.0"
      xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"&gt;
      &lt;saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"&gt;acme_tools_com&lt;/saml:Issuer&gt;
      &lt;samlp:RequestedAuthnContext Comparison="exact" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"&gt;
          &lt;saml:AuthnContextClassRef xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"&gt;http://schemas.microsoft.com/ws/2008/06/identity/authenticationmethod/password&lt;/saml:AuthnContextClassRef&gt;
      &lt;/samlp:RequestedAuthnContext&gt;
  &lt;/samlp:AuthnRequest&gt;
  </code></pre>
  
  <p>This SAML response is POSTED from the ADFS server to my callback <a href="https://localhost:3000/adfs/postResponse" rel="nofollow noreferrer">https://localhost:3000/adfs/postResponse</a>:</p>
  
  <pre><code>&lt;samlp:Response Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified"
      Destination="https://localhost:3000/adfs/postResponse" ID="_e543e979-0d99-48fe-947f-1d1469da8c70"
      InResponseTo="_49ab1e1060c3d7849902" IssueInstant="2018-06-28T19:46:27.782Z" Version="2.0"
      xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"&gt;
      &lt;Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion"&gt;http://nonp-adfs.dsgapps.dk/adfs/services/trust&lt;/Issuer&gt;
      &lt;samlp:Status&gt;&lt;samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/&gt;&lt;/samlp:Status&gt;
      &lt;Assertion ID="_cf245f57-1380-47cd-a5d3-05b13e4d9416" IssueInstant="2018-06-28T19:46:27.782Z"
          Version="2.0" xmlns="urn:oasis:names:tc:SAML:2.0:assertion"&gt;
          &lt;Issuer&gt;http://nonp-adfs.dsgapps.dk/adfs/services/trust&lt;/Issuer&gt;
          &lt;ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"&gt;
              &lt;ds:SignedInfo&gt;&lt;ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/&gt;&lt;ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/&gt;
                  &lt;ds:Reference URI="#_cf245f57-1380-47cd-a5d3-05b13e4d9416"&gt;
                      &lt;ds:Transforms&gt;&lt;ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/&gt;&lt;ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/&gt;&lt;/ds:Transforms&gt;&lt;ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/&gt;
                      &lt;ds:DigestValue&gt;r6voAsVq4yAJTn4BQLFsyaoiCK3b7KQbJ5jVqi53ceY=&lt;/ds:DigestValue&gt;
                  &lt;/ds:Reference&gt;
              &lt;/ds:SignedInfo&gt;
              &lt;ds:SignatureValue&gt;F55JA6jNp3qFfp7p/BSzQBRTtVPOlQvIfVNG3JiqjohVC7Et0+aiRVlHHvZNghPJxhmxhuAUbo2kOweN+lZKb+fqDgK51kZ/DrIVpkljmwP2gJYgOGpJti53wfH2qkdDsxNkR3e13mG7RKwBuA4gJ0NxUFshmxyun0HKefd10wjnFwHY6dELWFmTL1W5xd2ZF/98ahIaqEWAMCYsJewEg4ND8z4vG74miht3lWHfTJL6kQ0UGkTJVwGZy9L8zaY8AMDRujs8SlXvBx9nvUnvufpYqto4kd0O0USWMCOPipcF2sVYDOVzidRSRb79TK256Wg9EGiw1usVThfAJ8IBzQ==&lt;/ds:SignatureValue&gt;
              &lt;KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"&gt;
                  &lt;ds:X509Data&gt;
                      &lt;ds:X509Certificate&gt;MIIC5DCCAcygAwIBAgIQWKHI7vunT6hIeNtPiejvbTANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNBREZTIFNpZ25pbmcgLSBub25wLWFkZnMuZHNnYXBwcy5kazAeFw0xODA2MTUxNDQ4NTdaFw0xOTA2MTUxNDQ4NTdaMC4xLDAqBgNVBAMTI0FERlMgU2lnbmluZyAtIG5vbnAtYWRmcy5kc2dhcHBzLmRrMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyN0SSHVjJML1lmcHB8RBNLXnegEISB66Nc75xEpscFGNPKoQloLck6XLPYvhmiL8WiVHTzghiJpU/faViR7s+wksj3n4IXVfCxb6wMd78LiOfeE6yyED+C/EprwoRWGXncUK4lwfLDGOPbWVqaPy0u14rQR0mvn0BsIOiML1JJvAPtf8fhavNmce2aEeRltLY3N8aoLMw8/TMrG+wk1imUo+JScp3gOPqrDnQBGgcjdBY/EaC9mFfAUbhyly0vKl/gYkOv1HFhUMtH7NlLUmDsvOCt3Nrbf6aKmi+H1EAfwJR/POnMbsoC8sqf4PWk/kMtj1POOpZAnQOBE8u4NtPwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQDIdu6cr7LgoNdXpgtwd/Zt7sb1N6dJ/GgULuxBm7Bm1Mdsc9+Q0lDhxeGtay9AIvpbF67xvSzrCz3eL9xPuNV6BYmZpYFsyBPP4MROlkgq1MkqLDpkB/zkiKQqZiJG3RHl5e+WniFrAmNxuuUAtdhKbh1ADJKc1bxte6uiY0dN/Mfw6WnY3m3VOtae9xoqHNM2i4uhEbMvXV9Pmb8BVv4eIZLtOgo+vgkusp3FZa2PL4UWQIPNiEggIxhs7MfpaoADT4taGeavpHWKuxIGvDQzoe7GP2iDGzyH1kS24rSeJRYOiyBq1zPJHrSPeLFsef/7LapCaz5x5+T/eWPhyJKd&lt;/ds:X509Certificate&gt;
                  &lt;/ds:X509Data&gt;
              &lt;/KeyInfo&gt;
          &lt;/ds:Signature&gt;
          &lt;Subject&gt;
              &lt;SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"&gt;&lt;SubjectConfirmationData InResponseTo="_49ab1e1060c3d7849902"
                  NotOnOrAfter="2018-06-28T19:51:27.782Z" Recipient="https://localhost:3000/adfs/postResponse"/&gt;&lt;/SubjectConfirmation&gt;
          &lt;/Subject&gt;
          &lt;Conditions NotBefore="2018-06-28T19:46:27.781Z" NotOnOrAfter="2018-06-28T20:46:27.781Z"&gt;
              &lt;AudienceRestriction&gt;
                  &lt;Audience&gt;acme_tools_com&lt;/Audience&gt;
              &lt;/AudienceRestriction&gt;
          &lt;/Conditions&gt;
          &lt;AuthnStatement AuthnInstant="2018-06-28T19:45:51.797Z"&gt;
              &lt;AuthnContext&gt;
                  &lt;AuthnContextClassRef&gt;urn:oasis:names:tc:SAML:2.0:ac:classes:Password&lt;/AuthnContextClassRef&gt;
              &lt;/AuthnContext&gt;
          &lt;/AuthnStatement&gt;
      &lt;/Assertion&gt;
  &lt;/samlp:Response&gt;
  </code></pre>
  
  <p>NB: The SAML request and response can be inspected using the SAML Chrome extension. </p>
  
  <p>The most central part of my NodeJS program is this: </p>
  
  <pre><code>const verifyFunction = function(profile, done) {
      console.log("Verifying"+ profile);
      return done(null,
          {
              upn: profile['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn'],
              // e.g. if you added a Group claim
              group: profile['http://schemas.xmlsoap.org/claims/Group']
          });
  };
  
  var strategy = new SamlStrategy(
      {
          entryPoint: 'https://nonp-adfs.dsgapps.dk/adfs/ls',
          issuer: 'acme_tools_com',
          callbackUrl: 'https://localhost:3000/adfs/postResponse',
          privateCert: fs.readFileSync(root + '/acme_tools_com.key', 'utf-8'),
          cert: fs.readFileSync(root + '/acme_tools_com.cert', 'utf-8'),
          authnContext: 'http://schemas.microsoft.com/ws/2008/06/identity/authenticationmethod/password',
          // not sure if this is necessary?
          acceptedClockSkewMs: -1,
          identifierFormat: null,
          signatureAlgorithm: 'sha256'
      },
      verifyFunction
  );
  
  strategy.userProfile = function(accessToken, done) {
      console.log("UserProfile:" + accessToken);
      done(null, accessToken);
  };
  
  
  passport.use('provider', strategy);
  passport.serializeUser(function(user, done) {
      console.log("Serializing user");
      done(null, user);
  });
  passport.deserializeUser(function(user, done) {
      console.log("Deserializing user");
      done(null, user);
  });
  
  
  
  app.get('/login',
      passport.authenticate('provider', { failureRedirect: '/', failureFlash: true })
  );
  app.post('/adfs/postResponse',
      function(req, res, next) {
          console.log("Before authenticating: " );
          next();
      },
      passport.authenticate('provider', { failureRedirect: '/', failureFlash: true }),
      function(req, res) {
          console.log("User: " + util.inspect(req.user));
          res.cookie('accessToken', req.user);
          res.redirect('/');
      }
  );
  </code></pre>
  
  <p>When doing the login I see no errors on the ADFS server, so it looks good there. </p>
  
  <p>On the NodeJS server I see: </p>
  
  <pre><code>Express server started on https://localhost:3000 
  postResponse entered:
  postResponse entered:
  ...
  </code></pre>
  
  <p>In other words, verifyFunction is never called. Isn't that strange? It seems like the passport-saml module does not pick up the SAML response.  </p>
  
  <p>The POST from ADFS to my callback is the following: </p>
  
  <pre><code>curl 'https://localhost:3000/adfs/postResponse' -H 'Origin: https://nonp-adfs.dsgapps.dk' -H 'Content-Type: application/x-www-form-urlencoded' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8' -H 'Referer: https://nonp-adfs.dsgapps.dk/adfs/ls?SAMLRequest=nVJLc9owEP4rHt2xZMdD....' -H 'Accept-Encoding: gzip, deflate, br' -H 'Accept-Language: en-US,en;q=0.9,da;q=0.8' -H  --data 'SAMLResponse=PHNhbWxwOlJlc3.......' --compressed --insecure
  </code></pre>
  
  <p>This is the NodeJS program in full length: </p>
  
  <pre><code>'use strict';
  
  var util = require('util');
  var https = require('https');
  var app = require('express')();
  var cookieParser = require('cookie-parser');
  var passport = require('passport');
  var fs = require('fs');
  var SamlStrategy = require('passport-saml').Strategy;
  var path = require("path");
  
  const root = __dirname;
  const verifyFunction = function(profile, done) {
      console.log("Verifying"+ profile);
      return done(null,
          {
              upn: profile['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn'],
              // e.g. if you added a Group claim
              group: profile['http://schemas.xmlsoap.org/claims/Group']
          });
  };
  
  var strategy = new SamlStrategy(
      {
          entryPoint: 'https://nonp-adfs.dsgapps.dk/adfs/ls',
          issuer: 'acme_tools_com',
          callbackUrl: 'https://localhost:3000/adfs/postResponse',
          privateCert: fs.readFileSync(root + '/acme_tools_com.key', 'utf-8'),
          cert: fs.readFileSync(root + '/acme_tools_com.cert', 'utf-8'),
          authnContext: 'http://schemas.microsoft.com/ws/2008/06/identity/authenticationmethod/password',
          // not sure if this is necessary?
          acceptedClockSkewMs: -1,
          identifierFormat: null,
          signatureAlgorithm: 'sha256'
      },
      verifyFunction
  );
  
  strategy.userProfile = function(accessToken, done) {
      console.log("UserProfile:" + accessToken);
      done(null, accessToken);
  };
  
  
  passport.use('provider', strategy);
  passport.serializeUser(function(user, done) {
      console.log("Serializing user");
      done(null, user);
  });
  passport.deserializeUser(function(user, done) {
      console.log("Deserializing user");
      done(null, user);
  });
  
  function validateAccessToken(accessToken) {
      console.log("AccessToken: "+ accessToken);
      return;
  }
  
  
  // Configure express app
  app.use(cookieParser());
  app.use(passport.initialize());
  
  app.get('/login',
      passport.authenticate('provider', { failureRedirect: '/', failureFlash: true })
  );
  app.post('/adfs/postResponse',
      function(req, res, next) {
          console.log("Before authenticating: " );
          next();
      },
      passport.authenticate('provider', { failureRedirect: '/', failureFlash: true }),
      function(req, res) {
          console.log("User: " + util.inspect(req.user));
          res.cookie('accessToken', req.user);
          res.redirect('/');
      }
  );
  app.get('/', function (req, res) {
      req.user = req.cookies['accessToken'];
      res.send(
          !req.user ? '&lt;a href="/login"&gt;Log In&lt;/a&gt;' : '&lt;a href="/logout"&gt;Log Out&lt;/a&gt;' +
          '&lt;pre&gt;' + JSON.stringify(req.user, null, 2) + '&lt;/pre&gt;');
  });
  app.get('/logout', function (req, res) {
      res.clearCookie('accessToken');
      res.redirect('/');
  });
  
  var certOptions = {
      key: fs.readFileSync(root + '/localhost.key'),
      cert: fs.readFileSync(root + '/localhost.cert')
  }
  
  var server = https.createServer(certOptions, app).listen(3000)
  console.log('Express server started on https://localhost:3000');
  </code></pre>