4

Below is the error that I am getting on my console today as opposed to yesterday when the same code was working fine.

Error: SAML assertion not yet valid
        at SAML.checkTimestampsValidityError

I have verified that I receive a success from the IDP and hence the application gets redirected to the '/home' endpoint in the URL which has been mentioned in the config file.

Additionally, when I submit the form, after an auto redirection [which shows me Internal Server Error]

enter image description here

I press refresh button of the browser and a form submission happens and the expected result is achieved.

enter image description here

My problem is, why doesn't this happens automatically or how and where can I do this submission programatically.

passport.js

const SamlStrategy = require('passport-saml').Strategy;

module.exports = function (passport, config) {

  passport.serializeUser(function (user, done) {
    done(null, user);
  });

  passport.deserializeUser(function (user, done) {
    done(null, user);
  });

  passport.use(new SamlStrategy(
    {
      entryPoint: config.passport.saml.entryPoint,
      issuer: config.passport.saml.issuer,
      cert: config.passport.saml.cert,
      path: config.passport.saml.path,
      identifierFormat: config.passport.saml.identifierFormat
    },
    function (profile, done) {

      debugger;
      return done(null,
        {
          sessionIndex: profile.sessionIndex,
          nameID: profile.nameID,
          lastName: profile.lastName,
          firstName: profile.firstName,
          gid: profile.gid,
          county: profile.county,
          mail: profile.mail,
          companyUnit: profile.companyUnit,
          preferredLanguage: profile.preferredLanguage,
          orgCode: profile.orgCode,
          email: profile.email
        });
    })
  );

};

config.js

module.exports = {
      passport: {
        strategy: 'saml',
        saml: {
          callbackUrl: '/home',
          path: '/home',
          entryPoint: 'https://.../GetAccess/Saml/IDP/SSO/Unsolicited?GA_SAML_SP=APP',
          issuer: '...',
          cert: '...',
          identifierFormat: null
        }
      }
  };

app.js

import express from 'express';
import helmet from 'helmet';
import cookieParser from 'cookie-parser';
import bodyparser from 'body-parser';
import path from 'path';
import logger from 'morgan';
import cors from 'cors';
import passport from 'passport';
import session from 'cookie-session';

const config = require('./config.js');
require('./passport')(passport, config);
var app = express();
app.use(logger('dev'));
app.use(cookieParser());
app.use(bodyparser.json());
app.use(bodyparser.urlencoded({ extended: false }));
app.use('/public', express.static(path.join(__dirname, '../public')));
app.use('/data', express.static(path.join(__dirname, '../uploads/')));
app.use(session(
  {
    resave: true,
    saveUninitialized: true,
    secret: 'secret value'
  }));
app.use(passport.initialize());
app.use(passport.session());
app.use(helmet());
app.use(cors());

require('../router/routeConfig')(app, config, passport);

module.exports = app;

routeConfig.js

module.exports = function (app, config, passport) {


  app.get('/', passport.authenticate(config.passport.strategy, {
    successRedirect: '/home',
    failureRedirect: 'https://.../GetAccess/Saml/IDP/SSO/Unsolicited?GA_SAML_SP=APP'
  }));

  app.get('/app', passport.authenticate(config.passport.strategy, {
    successRedirect: '/home',
    failureRedirect: 'https://.../GetAccess/Saml/IDP/SSO/Unsolicited?GA_SAML_SP=APP'
  }));

  app.post(config.passport.saml.path,
    passport.authenticate(config.passport.strategy,
      {
        failureRedirect: 'https://.../GetAccess/Saml/IDP/SSO/Unsolicited?GA_SAML_SP=APP',
        failureFlash: true
      }),
    function (req, res) {
      debugger;
      res.sendFile(path.join(__dirname, "../public/index.html"));
    });

};
Prateek
  • 3,923
  • 6
  • 41
  • 79
  • In my case I had been testing with my system clock set to a different time than the real time. Once I changed that back it worked. I also deleted any cookies to do with the IdP and restarted my local dev server. – ABCD.ca Nov 13 '22 at 01:21

3 Answers3

4

As described above, adding 'acceptedClockSkewMs: -1' to your passport-saml strategy configuration resolves the error.

example:

const strategy = new SamlStrategy(
  {
    path: "/api/auth/callback",
    entryPoint: process.env.SAML_ENTRY_POINT, // identity provider entrypoint
    issuer: issuer,
    cert: process.env.SAML_CERT,
    acceptedClockSkewMs: -1
  }, function(profile, done){...}

EDIT: Looking back at this answer I should have gone into more detail and highlighted the pros/cons and different approaches.

Do not use the suggested -1 or default 0 from the passport-saml documentation. Increase the ms time to 10 or tinker with the value until it suits your environment and latency between server, SP and IDP.

adR
  • 450
  • 6
  • 11
3

Finally I have figured this out after some research,

As we understand that there are two parties involved in the SAML Authentication Process i.e. IDP and SP, therefore there are certain conditions between these which are supposed to be met, as part of the contract. One such condition is the TIME.

<saml:Conditions NotBefore="2019-08-01T11:02:49Z" NotOnOrAfter="2019-08-01T11:03:59Z">

This is a tag that I have clipped from the saml response received from the IDP, here the time of my(SP's) server should be between NotBefore and NotOnOrAfter during the process of authentication.

Therefore, I need to calibrate the clock of my server by a few seconds so that I fit in the time-slice of NotBefore and NotOnOrAfter of the server.

Of course that is not the way this should be done, but some +n or -n minutes should be allowed from the IDP side (importantly both SP and IDP to follow UTC times).

More on this topic can be found here,

SAML Assertion NotBefore,NotOnOrAfter problem due to unsynced clocks : Explained

ADFS Not Before Time Skew

Bonus

Base 64 to XML Decoder

XML Prettifier

Edit :

As mentioned in the comment below, The skew can be configured on either side (IdP > or SP) or both sides. From passport-saml docs: acceptedClockSkewMs: Time in milliseconds of skew that is acceptable between client and server when checking OnBefore and NotOnOrAfter assertion condition validity timestamps. Setting to -1 will disable checking these conditions entirely. Default is 0.

Mentioned Here

Community
  • 1
  • 1
Prateek
  • 3,923
  • 6
  • 41
  • 79
  • 1
    The skew can be configured on either side (IdP or SP) or both sides. From passport-saml docs: _acceptedClockSkewMs: Time in milliseconds of skew that is acceptable between client and server when checking OnBefore and NotOnOrAfter assertion condition validity timestamps. Setting to -1 will disable checking these conditions entirely. Default is 0_ – identigral Aug 02 '19 at 17:52
  • Thanks for redirecting me to the documentation of passport-saml, as I couldn't always chase the server times with each deployment happening, this would be the way it should be done. – Prateek Aug 18 '19 at 13:38
  • This should be the accepted answer, increase the ms time to suit the latency for the round trip between your server and the IDP/SDP. Adjust the value to 10 and tinker around to what best suits. – adR Nov 06 '22 at 22:40
1

I do not have enough reputation to add comment to @adR 's answer (which instructed to set acceptedClockSkewMs to -1 in order to fix the problem) so I'm posting a separate answer.

Setting acceptedClockSkewMs to -1 is not proper a fix at all. It opens a replay attack vector.

The reason is that passport-saml skips NotOnOrAfter validation if acceptedClockSkewMs is set to -1.

A proper fix is (see @Prateek 's answer for more information): Keep the clocks in sync by using e.g. NTP and use acceptedClockSkewMs to finetune small time differences (e.g. 30 seconds).

By disabling the NotOnOrAfter check, an attacker may replay a stored SAML login response forever without ever having to authenticate at IdP anymore (i.e. it would be possible to gain access to SP even after account at IdP side is terminated).

Side note: If a disabled NotOnOrAfter validation is combined with a disabled audience validation (it is disabled by default in passport-saml meaning that it is disabled in @adR 's example also) any stored saml authentication response from any SP (which share same IdP) can be used to gain access by replaying login response to site with aforementioned disabled checks.

Agneum
  • 727
  • 7
  • 23
jkginkih
  • 36
  • 2
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Apr 01 '22 at 11:46
  • I have adjusted my answer based on @Prateek and your comment. My answer was based on the passport-saml documentation but has been adjusted to address security concerns and latency. – adR Nov 06 '22 at 22:42