passport-saml - express - redirected url not submitting form gives SAML assertion not yet valid
Asked Answered
C

3

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"));
    });

};
Caddric answered 31/7, 2019 at 12:40 Comment(1)
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.Deepdyed
C
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

Caddric answered 1/8, 2019 at 11:47 Comment(3)
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 0Cheryle
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.Caddric
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.Mouser
M
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.

Mouser answered 23/3, 2020 at 2:21 Comment(0)
C
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

Caddric answered 1/8, 2019 at 11:47 Comment(3)
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 0Cheryle
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.Caddric
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.Mouser
T
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.

Tamanaha answered 1/4, 2022 at 11:26 Comment(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.Katharinakatharine
I have adjusted my answer based on @Caddric and your comment. My answer was based on the passport-saml documentation but has been adjusted to address security concerns and latency.Mouser

© 2022 - 2024 — McMap. All rights reserved.