passport-saml - express - redirected url 未提交表单给出 SAML 断言尚未生效
passport-saml - express - redirected url not submitting form gives SAML assertion not yet valid
下面是我今天在我的控制台上遇到的错误,而不是昨天当相同代码运行良好时出现的错误。
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.
此外,当我提交表单时,在自动重定向后[显示 内部服务器错误]
我按下浏览器的刷新按钮,然后提交了一个表单,达到了预期的结果。
我的问题是,为什么这不会自动发生,或者我如何以及在何处以编程方式进行此提交。
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"));
});
};
经过一番研究,我终于弄明白了,
据我们了解,SAML 身份验证过程涉及两方,即 IDP 和 SP,因此两者之间存在某些条件作为合同的一部分,这些应该得到满足。一个这样的条件是 TIME.
<saml:Conditions NotBefore="2019-08-01T11:02:49Z" NotOnOrAfter="2019-08-01T11:03:59Z">
这是我从 IDP 收到的 saml 响应中截取的标签,这里我的(SP 的)服务器的时间应该在 NotBefore 之间 和 NotOnOrAfter 在验证过程中。
因此,我需要将我的服务器时钟校准几秒钟,以便我适合 NotBefore 和 NotOnOrAfter[= 的时间片服务器的 49=]。
当然这不是应该这样做的方式,但是 IDP 端应该允许一些 +n 或 -n 分钟 (重要的是 SP 和 IDP 都遵循 UTC 时间).
可以在此处找到有关此主题的更多信息,
SAML Assertion NotBefore,NotOnOrAfter problem due to unsynced clocks : Explained
Bonus
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.
如上所述,将 'acceptedClockSkewMs: -1' 添加到您的 passport-saml 策略配置中可以解决错误。
示例:
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 // SAML assertion not yet valid fix
}, function(profile, done){...}
我没有足够的声誉向@adR 的回答添加评论(指示将 acceptedClockSkewMs
设置为 -1
以解决问题)所以我发布了一个单独的回答。
将 acceptedClockSkewMs
设置为 -1
根本不是正确的修复方法。它打开了一个重放攻击向量。
原因是如果 acceptedClockSkewMs
设置为 -1
,passport-saml
会跳过 NotOnOrAfter
验证。
正确的解决方法是(有关更多信息,请参阅@Prateek 的回答):通过使用例如NTP 并使用 acceptedClockSkewMs
微调小的时间差异(例如 30 秒)。
通过禁用 NotOnOrAfter
检查,攻击者可以永远重放存储的 SAML 登录响应,而无需再在 IdP 进行身份验证(即,即使在 IdP 端的帐户之后,也有可能获得对 SP 的访问权限已终止)。
旁注:如果禁用的 NotOnOrAfter
验证与禁用的 audience
验证结合使用(在 passport-saml
中默认禁用,这意味着它在 @adR 的也是示例)来自任何 SP(共享相同的 IdP)的任何存储的 saml 身份验证响应都可以用于通过重播对具有上述禁用检查的站点的登录响应来获得访问权限。
下面是我今天在我的控制台上遇到的错误,而不是昨天当相同代码运行良好时出现的错误。
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.
此外,当我提交表单时,在自动重定向后[显示 内部服务器错误]
我按下浏览器的刷新按钮,然后提交了一个表单,达到了预期的结果。
我的问题是,为什么这不会自动发生,或者我如何以及在何处以编程方式进行此提交。
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"));
});
};
经过一番研究,我终于弄明白了,
据我们了解,SAML 身份验证过程涉及两方,即 IDP 和 SP,因此两者之间存在某些条件作为合同的一部分,这些应该得到满足。一个这样的条件是 TIME.
<saml:Conditions NotBefore="2019-08-01T11:02:49Z" NotOnOrAfter="2019-08-01T11:03:59Z">
这是我从 IDP 收到的 saml 响应中截取的标签,这里我的(SP 的)服务器的时间应该在 NotBefore 之间 和 NotOnOrAfter 在验证过程中。
因此,我需要将我的服务器时钟校准几秒钟,以便我适合 NotBefore 和 NotOnOrAfter[= 的时间片服务器的 49=]。
当然这不是应该这样做的方式,但是 IDP 端应该允许一些 +n 或 -n 分钟 (重要的是 SP 和 IDP 都遵循 UTC 时间).
可以在此处找到有关此主题的更多信息,
SAML Assertion NotBefore,NotOnOrAfter problem due to unsynced clocks : Explained
Bonus
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.
如上所述,将 'acceptedClockSkewMs: -1' 添加到您的 passport-saml 策略配置中可以解决错误。
示例:
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 // SAML assertion not yet valid fix
}, function(profile, done){...}
我没有足够的声誉向@adR 的回答添加评论(指示将 acceptedClockSkewMs
设置为 -1
以解决问题)所以我发布了一个单独的回答。
将 acceptedClockSkewMs
设置为 -1
根本不是正确的修复方法。它打开了一个重放攻击向量。
原因是如果 acceptedClockSkewMs
设置为 -1
,passport-saml
会跳过 NotOnOrAfter
验证。
正确的解决方法是(有关更多信息,请参阅@Prateek 的回答):通过使用例如NTP 并使用 acceptedClockSkewMs
微调小的时间差异(例如 30 秒)。
通过禁用 NotOnOrAfter
检查,攻击者可以永远重放存储的 SAML 登录响应,而无需再在 IdP 进行身份验证(即,即使在 IdP 端的帐户之后,也有可能获得对 SP 的访问权限已终止)。
旁注:如果禁用的 NotOnOrAfter
验证与禁用的 audience
验证结合使用(在 passport-saml
中默认禁用,这意味着它在 @adR 的也是示例)来自任何 SP(共享相同的 IdP)的任何存储的 saml 身份验证响应都可以用于通过重播对具有上述禁用检查的站点的登录响应来获得访问权限。