node.jsshibbolethpassport-saml

How to provide SP metadata to TestShib IdP using passport-saml?


I have a node.js script, using passport-saml, that simulates an SP. My goal is to connect it to this TestShib IdP but I get the following error: SAML 2 SSO profile is not configured for relying party.

Based on what I read here, I know that I need to provide the SP metadata, but I do not know how. I know that passport-saml has the function: generateServiceProviderMetadata(decryptionCert) and I while I have the certificates required, I do not know how to make it all work.

Also, I want to avoid having to register my SP if that is possible.

Here is my script:

const https = require('https');
const fs = require('fs');
const express = require('express');
const morgan = require('morgan');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const passport = require('passport');
const saml = require('passport-saml');


/*
---------------------------------------------------------------------------------------------------
--  certificates
---------------------------------------------------------------------------------------------------
*/

//  for https server
const https_cert = fs.readFileSync('certificate.pem', 'utf-8');
const https_pvk = fs.readFileSync('privatekey.pem', 'utf-8');

//  from idp's metadata
const idp_cert_1 = 'MIIDAzCCAeugAwIBAgIVAPX0G6LuoXnKS0Muei006mVSBXbvMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNVBAMMEGlkcC50ZXN0c2hpYi5vcmcwHhcNMTYwODIzMjEyMDU0WhcNMzYwODIzMjEyMDU0WjAbMRkwFwYDVQQDDBBpZHAudGVzdHNoaWIub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAg9C4J2DiRTEhJAWzPt1S3ryhm3M2P3hPpwJwvt2q948vdTUxhhvNMuc3M3S4WNh6JYBs53R+YmjqJAII4ShMGNEmlGnSVfHorex7IxikpuDPKV3SNf28mCAZbQrX+hWA+ann/uifVzqXktOjs6DdzdBnxoVhniXgC8WCJwKcx6JO/hHsH1rG/0DSDeZFpTTcZHj4S9MlLNUtt5JxRzV/MmmB3ObaX0CMqsSWUOQeE4nylSlp5RWHCnx70cs9kwz5WrflnbnzCeHU2sdbNotBEeTHot6a2cj/pXlRJIgPsrL/4VSicPZcGYMJMPoLTJ8mdy6mpR6nbCmP7dVbCIm/DQIDAQABoz4wPDAdBgNVHQ4EFgQUUfaDa2mPi24x09yWp1OFXmZ2GPswGwYDVR0RBBQwEoIQaWRwLnRlc3RzaGliLm9yZzANBgkqhkiG9w0BAQsFAAOCAQEASKKgqTxhqBzROZ1eVy++si+eTTUQZU4+8UywSKLia2RattaAPMAcXUjO+3cYOQXLVASdlJtt+8QPdRkfp8SiJemHPXC8BES83pogJPYEGJsKo19l4XFJHPnPy+Dsn3mlJyOfAa8RyWBS80u5lrvAcr2TJXt9fXgkYs7BOCigxtZoR8flceGRlAZ4p5FPPxQR6NDYb645jtOTMVr3zgfjP6Wh2dt+2p04LG7ENJn8/gEwtXVuXCsPoSCDx9Y0QmyXTJNdV1aB0AhORkWPlFYwp+zOyOIR+3m1+pqWFpn0eT/HrxpdKa74FA3R2kq4R7dXe4G0kUgXTdqXMLRKhDgdmA==';
const idp_cert_2 = 'MIIDAzCCAeugAwIBAgIVAPX0G6LuoXnKS0Muei006mVSBXbvMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNVBAMMEGlkcC50ZXN0c2hpYi5vcmcwHhcNMTYwODIzMjEyMDU0WhcNMzYwODIzMjEyMDU0WjAbMRkwFwYDVQQDDBBpZHAudGVzdHNoaWIub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAg9C4J2DiRTEhJAWzPt1S3ryhm3M2P3hPpwJwvt2q948vdTUxhhvNMuc3M3S4WNh6JYBs53R+YmjqJAII4ShMGNEmlGnSVfHorex7IxikpuDPKV3SNf28mCAZbQrX+hWA+ann/uifVzqXktOjs6DdzdBnxoVhniXgC8WCJwKcx6JO/hHsH1rG/0DSDeZFpTTcZHj4S9MlLNUtt5JxRzV/MmmB3ObaX0CMqsSWUOQeE4nylSlp5RWHCnx70cs9kwz5WrflnbnzCeHU2sdbNotBEeTHot6a2cj/pXlRJIgPsrL/4VSicPZcGYMJMPoLTJ8mdy6mpR6nbCmP7dVbCIm/DQIDAQABoz4wPDAdBgNVHQ4EFgQUUfaDa2mPi24x09yWp1OFXmZ2GPswGwYDVR0RBBQwEoIQaWRwLnRlc3RzaGliLm9yZzANBgkqhkiG9w0BAQsFAAOCAQEASKKgqTxhqBzROZ1eVy++si+eTTUQZU4+8UywSKLia2RattaAPMAcXUjO+3cYOQXLVASdlJtt+8QPdRkfp8SiJemHPXC8BES83pogJPYEGJsKo19l4XFJHPnPy+Dsn3mlJyOfAa8RyWBS80u5lrvAcr2TJXt9fXgkYs7BOCigxtZoR8flceGRlAZ4p5FPPxQR6NDYb645jtOTMVr3zgfjP6Wh2dt+2p04LG7ENJn8/gEwtXVuXCsPoSCDx9Y0QmyXTJNdV1aB0AhORkWPlFYwp+zOyOIR+3m1+pqWFpn0eT/HrxpdKa74FA3R2kq4R7dXe4G0kUgXTdqXMLRKhDgdmA==';


/*
---------------------------------------------------------------------------------------------------
--  passport-saml setup
---------------------------------------------------------------------------------------------------
*/
const saml_strategy = new saml.Strategy(
    {
        'callbackUrl': 'https://localhost:44300/login/callback',
        'entryPoint': 'https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO',
        'issuer': 'https://localhost:44300',
        'decryptionPvk': https_pvk,
        'cert': [idp_cert_1, idp_cert_2]
    }, 
    function (profile, done) 
    {
        console.log('passport.use() profile: %s \n', JSON.stringify(profile));
        return done(
            null,
            { 
                'nameIDFormat': profile.nameIDFormat, 
                'nameID': profile.nameID 
            }
        );
    }
);

passport.serializeUser(function (user, done) {
    console.log('passport.serializeUser() user: %s \n', JSON.stringify(user));
    done(null, user);
});

passport.deserializeUser(function (user, done) {
    console.log('passport.deserializeUser() user: %s \n', JSON.stringify(user));
    done(null, user);
});

passport.use(saml_strategy);

/*
---------------------------------------------------------------------------------------------------
--  express setup
---------------------------------------------------------------------------------------------------
*/
const app = express();

// configure view engine to render EJS templates
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');

// additional settings for logging and parsing
app.use(morgan('dev'));
app.use(cookieParser());
app.use(bodyParser.urlencoded({'extended': true}));
app.use(session({ 'secret': 'this-is-secret', 'resave': false, 'saveUninitialized': false }));

// initialize Passport and restore authentication state, if any, from the session
app.use(passport.initialize());
app.use(passport.session());


/*
---------------------------------------------------------------------------------------------------
--  routes
---------------------------------------------------------------------------------------------------
*/

app.get(
    '/', 
    function (req, res) {
        if (req.isAuthenticated()) {
            console.log('GET [/] user authenticated! req.user: %s \n', JSON.stringify(req.user));
            res.render('home', { 'user': req.user });
        } else {
            console.log('GET [/] user not authenticated! \n');
            res.render('home', { 'user': null });
        }
    }
);

app.get(
    '/login', 
    passport.authenticate('saml', { 'successRedirect': '/', 'failureRedirect': '/login' })
);


app.post(
    '/login/callback',
    passport.authenticate('saml', { 'failureRedirect': '/', 'failureFlash': true }),
    function(req, res) {
        console.log('POST [/login] \n');
        res.redirect('/');
    }
);


app.get(
    '/profile',
    function(req, res){
        if (req.isAuthenticated()) {
            console.log('GET [/profile] user authenticated! req.user: %s \n', JSON.stringify(req.user));
            res.render('profile', { 'user': req.user });
        } else {
            console.log('GET [/profile] user not authenticated! \n');
            res.redirect('/login');
        }
    }
);


app.get(
    '/logout',
    function(req, res) {
        console.log('GET [/logout] \n');
        passport._strategy('saml').logout(
            req, 
            function(err, requestUrl) {
                req.logout();
                res.redirect('/');
            }
        );
    }
);

/*
---------------------------------------------------------------------------------------------------
--  start https server
---------------------------------------------------------------------------------------------------
*/

const server = https.createServer({
    'key': https_pvk,
    'cert': https_cert
}, app);

server.listen(44300, function() {
    console.log('Listening on https://localhost:%d', server.address().port)
});

Thank you in advance for your help and guidance!


Solution

  • I know that passport-saml has the function: generateServiceProviderMetadata(decryptionCert) and I while I have the certificates required, I do not know how to make it all work.

    For the sake of completeness, I'll list all the steps needed. But you've already done the first 2 so you can skip them:

    1. Create the cert/key

    2. In your SamlStrategy, define decryptionPvk

      decryptionPvk: fs.readFileSync('./credentials/key.pem', 'utf-8'),
      
    3. Lastly, you can create an endpoint that exposes your metadata (this is the part you're missing)

      app.get('/metadata',
        function(req, res) {
          const decryptionCert = fs.readFileSync('./credentials/cert.pem', 'utf-8');
          res.type('application/xml');
          res.send((myStrategy.generateServiceProviderMetadata(decryptionCert)));
        }
      );
      

      Here's what that would look like reformatted to match the sample code you posted:

      app.get(
          '/metadata',
          function(req, res) {
              res.type('application/xml');
              res.send((saml_strategy.generateServiceProviderMetadata(https_cert)));
          }
      );
      

    Once that's done you should be able to access your metadata from the /metadata path.

    Also, I want to avoid having to register my SP if that is possible.

    I'm not a SAML expert, but you probably won't be able to avoid registration. This would only work with an IdP that was set up to trust all SPs, but that wouldn't make much sense because a key part of SAML is the two-way trust that's established between the IdP and SP.