我已经在 NodeJS 应用程序上使用 express、passport 和passport-saml 实现了带有 ADFS 的 SSO。我可以登录 ADFS,并且我与 SAML 令牌一起正确回发到我的回调路由(即 localhost:3000/adfs/postResponse(。但是,当我到达回调路由时,SAML 令牌似乎被拒绝了,所以我被送回 ADFS 登录。然后重复此操作。
谁能建议可能出了什么问题?任何帮助将不胜感激。
更多详情:
此 SAML 请求将发送到 ADFS:
<?xml version="1.0"?>
<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">
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">acme_tools_com</saml:Issuer>
<samlp:RequestedAuthnContext Comparison="exact" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
<saml:AuthnContextClassRef xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">http://schemas.microsoft.com/ws/2008/06/identity/authenticationmethod/password</saml:AuthnContextClassRef>
</samlp:RequestedAuthnContext>
</samlp:AuthnRequest>
此 SAML 响应从 ADFS 服务器发布到我的回调 https://localhost:3000/adfs/postResponse:
<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">
<Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">http://nonp-adfs.dsgapps.dk/adfs/services/trust</Issuer>
<samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status>
<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">
<Issuer>http://nonp-adfs.dsgapps.dk/adfs/services/trust</Issuer>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<ds:Reference URI="#_cf245f57-1380-47cd-a5d3-05b13e4d9416">
<ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>r6voAsVq4yAJTn4BQLFsyaoiCK3b7KQbJ5jVqi53ceY=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>F55JA6jNp3qFfp7p/BSzQBRTtVPOlQvIfVNG3JiqjohVC7Et0+aiRVlHHvZNghPJxhmxhuAUbo2kOweN+lZKb+fqDgK51kZ/DrIVpkljmwP2gJYgOGpJti53wfH2qkdDsxNkR3e13mG7RKwBuA4gJ0NxUFshmxyun0HKefd10wjnFwHY6dELWFmTL1W5xd2ZF/98ahIaqEWAMCYsJewEg4ND8z4vG74miht3lWHfTJL6kQ0UGkTJVwGZy9L8zaY8AMDRujs8SlXvBx9nvUnvufpYqto4kd0O0USWMCOPipcF2sVYDOVzidRSRb79TK256Wg9EGiw1usVThfAJ8IBzQ==</ds:SignatureValue>
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>MIIC5DCCAcygAwIBAgIQWKHI7vunT6hIeNtPiejvbTANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNBREZTIFNpZ25pbmcgLSBub25wLWFkZnMuZHNnYXBwcy5kazAeFw0xODA2MTUxNDQ4NTdaFw0xOTA2MTUxNDQ4NTdaMC4xLDAqBgNVBAMTI0FERlMgU2lnbmluZyAtIG5vbnAtYWRmcy5kc2dhcHBzLmRrMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyN0SSHVjJML1lmcHB8RBNLXnegEISB66Nc75xEpscFGNPKoQloLck6XLPYvhmiL8WiVHTzghiJpU/faViR7s+wksj3n4IXVfCxb6wMd78LiOfeE6yyED+C/EprwoRWGXncUK4lwfLDGOPbWVqaPy0u14rQR0mvn0BsIOiML1JJvAPtf8fhavNmce2aEeRltLY3N8aoLMw8/TMrG+wk1imUo+JScp3gOPqrDnQBGgcjdBY/EaC9mFfAUbhyly0vKl/gYkOv1HFhUMtH7NlLUmDsvOCt3Nrbf6aKmi+H1EAfwJR/POnMbsoC8sqf4PWk/kMtj1POOpZAnQOBE8u4NtPwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQDIdu6cr7LgoNdXpgtwd/Zt7sb1N6dJ/GgULuxBm7Bm1Mdsc9+Q0lDhxeGtay9AIvpbF67xvSzrCz3eL9xPuNV6BYmZpYFsyBPP4MROlkgq1MkqLDpkB/zkiKQqZiJG3RHl5e+WniFrAmNxuuUAtdhKbh1ADJKc1bxte6uiY0dN/Mfw6WnY3m3VOtae9xoqHNM2i4uhEbMvXV9Pmb8BVv4eIZLtOgo+vgkusp3FZa2PL4UWQIPNiEggIxhs7MfpaoADT4taGeavpHWKuxIGvDQzoe7GP2iDGzyH1kS24rSeJRYOiyBq1zPJHrSPeLFsef/7LapCaz5x5+T/eWPhyJKd</ds:X509Certificate>
</ds:X509Data>
</KeyInfo>
</ds:Signature>
<Subject>
<SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><SubjectConfirmationData InResponseTo="_49ab1e1060c3d7849902"
NotOnOrAfter="2018-06-28T19:51:27.782Z" Recipient="https://localhost:3000/adfs/postResponse"/></SubjectConfirmation>
</Subject>
<Conditions NotBefore="2018-06-28T19:46:27.781Z" NotOnOrAfter="2018-06-28T20:46:27.781Z">
<AudienceRestriction>
<Audience>acme_tools_com</Audience>
</AudienceRestriction>
</Conditions>
<AuthnStatement AuthnInstant="2018-06-28T19:45:51.797Z">
<AuthnContext>
<AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</AuthnContextClassRef>
</AuthnContext>
</AuthnStatement>
</Assertion>
</samlp:Response>
注意:可以使用 SAML Chrome 扩展程序检查 SAML 请求和响应。
我的 NodeJS 程序最核心的部分是:
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('/');
}
);
登录时,我在 ADFS 服务器上没有看到任何错误,因此在那里看起来不错。
在 NodeJS 服务器上,我看到:
Express server started on https://localhost:3000
postResponse entered:
postResponse entered:
...
换句话说,永远不会调用验证函数。这不是很奇怪吗?似乎护照 SAML 模块没有拾取 SAML 响应。
从 ADFS 到我的回调的 POST 如下:
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
这是完整的 NodeJS 程序:
'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 ? '<a href="/login">Log In</a>' : '<a href="/logout">Log Out</a>' +
'<pre>' + JSON.stringify(req.user, null, 2) + '</pre>');
});
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');
问题变得非常简单。我需要包含中间件来解析 POST 数据。
将这些行添加到我的 NodeJS 模块的顶部后,它起作用了。
var bodyParser = require('body-parser');
app.use(bodyParser.urlencoded());
app.use(bodyParser.json());