Currently I am working on a passport-saml implementation in our NodeJS application.
The reason to do so is to give our customers the possibility to connect to their AD FS systems and take advantage of SingleSignOn(SSO).
As we also want to give logout functionality I was working on that logic. However, I can't seem to get this simple piece of functionality working. I have already googled a lot, tried a lot of variations and configurations but unfortunately, it does not work.
I would like to give our customers the possibility to SingleLogOut (SLO) that is both SP and IdP driven. This was my starting point. During the course of debugging and development I already took a step back and tried to kill the local session but even that is not possible it seems.
This is the relevant code from the routes I configured for SAML:
const isAuthenticated = (req, res, next) => {
if (req.isAuthenticated()) {
// User logged in, pass on to next middleware
console.info('User authenticated');
return next();
}
// User not logged in, redirect to login page
console.info('User not authenticated');
return res.redirect('/login');
};
// GET-routes
app.get('/',
isAuthenticated,
(req, res) => {
res.send('Authenticated');
});
app.get('/login',
passport.authenticate('saml', {
successRedirect: '/',
failureRedirect: '/login/fail',
}));
app.get('/logout',
(req, res) => {
passport._strategy('saml').logout(req, (err, url) => {
return res.redirect(url);
});
});
// POST-routes
app.post('/adfs/callback',
(req, res, next) => {
passport.authenticate('saml', (err, user) => {
// If error occurred redirect to failure URL
if (err) return res.redirect('/login/fail');
// If no user could be found, redirect to failure URL
if (!user) return res.redirect('/login/fail');
// User found, handle registration of user on request
req.logIn(user, (loginErr) => {
if (loginErr) return res.status(400).send(err);
// Request session set, put in store
store.set(req.sessionID, req.session, (storeErr) => {
if (storeErr) return res.status(400).send(storeErr);
return res.redirect('/');
});
});
})(req, res, next);
});
app.post('/logout/callback', (req, res) => {
// Destroy session and cookie
store.destroy(req.sessionID, async (err) => {
req.logout();
return res.redirect('/');
});
});
As can be seen I took control of the session store handling (setting and destroying sessions, but if this is unwise please advise).
The session store implemented is the MemoryStore (https://www.npmjs.com/package/memorystore).
What happens is that, when a user is logged in everything works fine.
Then a request is sent to route /logout, and some stuff happend and I can see the session changing, the session ID gets changed as well as relevant parameters for passport-saml (nameID, sessionIndex) and the user is then rerouted to '/'.
However then the user is seen as not authenticated and rerouted to '/login'. One would argue that it stops here, as the credentials have to be re-entered.
This is not the case, as the user is directly logged in again, without re-entering credentials and I do not know how to prevent this.
I do hope anybody knows what's going on :)
If there is need for additional information I would like to hear gladly.
So after much research and investigation I did found the solution for this problem.
The trick was in the definition of the passport-saml package, in particular the authncontext parameter.
So previously I had the SamlStrategy options defined as:
{
// URL that should be configured inside the AD FS as return URL for authentication requests
callbackUrl: `<URL>`,
// URL on which the AD FS should be reached
entryPoint: <URL>,
// Identifier for the CIR-COO application in the AD FS
issuer: <identifier>,
identifierFormat: null,
// CIR-COO private certificate
privateCert: <private_cert_path>,
// Identity Provider's public key
cert: <cert_path>,
authnContext: ["urn:federation:authentication:windows"],
// AD FS signature hash algorithm with which the response is encrypted
signatureAlgorithm: <algorithm>,
// Single Log Out URL AD FS
logoutUrl: <URL>,
// Single Log Out callback URL
logoutCallbackUrl: `<URL>`,
}
But after much research I realised this authentication:windows option was the culprit, so I changed it to:
{
// URL that should be configured inside the AD FS as return URL for authentication requests
callbackUrl: `<URL>`,
// URL on which the AD FS should be reached
entryPoint: <URL>,
// Identifier for the CIR-COO application in the AD FS
issuer: <identifier>,
identifierFormat: null,
// CIR-COO private certificate
privateCert: <private_cert_path>,
// Identity Provider's public key
cert: <cert_path>,
authnContext: ["urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport",
"urn:federation:authentication:windows"],
// AD FS signature hash algorithm with which the response is encrypted
signatureAlgorithm: <algorithm>,
// Single Log Out URL AD FS
logoutUrl: <URL>,
// Single Log Out callback URL
logoutCallbackUrl: `<URL>`,
},
Which basically means it won't retrieve the Windows credentials of the user that is logged onto the system by default, thus redirecting to the login screen of the ADFS server.
Using authnContext: ["urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"] in the session setup will show the username and password page again after logging out.
Logout achieved https://adfs-url/adfs/ls/?wa=wsignout1.0.
A new session-id is created after signing in again.
Related
I have just completed my signup auth with passport.js but I kept on getting error when I was trying to use the login auth
Error: Failed to serialize user into session
This was my post route :
router.post("/login",passport.authenticate('local-login'),function(req,res){
res.redirect("/users")
if (req.user) {
console.log("Logged In!")
} else {
console.log("Not logged in!")
}
})
I saw a comment on stackoverflow that says we need to do:
app.post('/login', passport.authenticate('local', {
successRedirect: '/accessed',
failureRedirect: '/access',
session: false
}));
In the login route.
Using the code above does solve the error message.Maybe this is my poor understanding of passport authentication but isn't the point of going through the login to store the user info in the session. If we set session to false how do we store the user info?
This is taken from the docs of passport.js.
Disable Sessions
After successful authentication, Passport will establish a persistent
login session. This is useful for the common scenario of users
accessing a web application via a browser. However, in some cases,
session support is not necessary. For example, API servers typically
require credentials to be supplied with each request. When this is the
case, session support can be safely disabled by setting the session
option to false.
So basically, the difference is that for clients such as browsers, you usually want to have session persistence. For cases when you're calling internal APIs and don't really need persistence, you disable sessions.
Try Below :
Please make sure you use newest version of passport (which is 0.2.1 for today).
Please try passing { session: false } as a second parameter of your req.logIn() function:
app.get('/login', function (req, res, next) {
passport.authenticate('local', function (err, user, info) {
if (err) { return next(err); }
if (!user) { return res.redirect('/login'); }
req.logIn(user, { session: false }, function (err) {
// Should not cause any errors
if (err) { return next(err); }
return res.redirect('/users/' + user.username);
});
})(req, res, next);
});
Open this below link for more :
PassportJS - Custom Callback and set Session to false
I've been using Passport on my server for user authentication.
When a user is signing in locally (using a username and password), the server sends them a JWT which is stored in localstorage, and is sent back to server for every api call that requires user authentication.
Now I want to support Facebook and Google login as well. Since I began with Passport I thought it would be best to continue with Passport strategies, using passport-facebook and passport-google-oauth.
I'll refer to Facebook, but both strategies behave the same. They both require redirection to a server route ('/auth/facebook' and '/auth/facebook/callback' for that matter).
The process is successful to the point of saving users including their facebook\google ids and tokens on the DB.
When the user is created on the server, a JWT is created (without any reliance on the token received from facebook\google).
... // Passport facebook startegy
var newUser = new User();
newUser.facebook = {};
newUser.facebook.id = profile.id;
newUser.facebook.token = token; // token received from facebook
newUser.facebook.name = profile.displayName;
newUser.save(function(err) {
if (err)
throw err;
// if successful, return the new user
newUser.jwtoken = newUser.generateJwt(); // JWT CREATION!
return done(null, newUser);
});
The problem is that after its creation, I don't find a proper way to send the JWT to the client, since I should also redirect to my app.
app.get('/auth/facebook/callback',
passport.authenticate('facebook', {
session: false,
successRedirect : '/',
failureRedirect : '/'
}), (req, res) => {
var token = req.user.jwtoken;
res.json({token: token});
});
The code above redirects me to my app main page, but I don't get the token.
If I remove the successRedirect, I do get the token, but I'm not redirected to my app.
Any solution for that? Is my approach wrong? Any suggestions will do.
The best solution I found for that problem would be to redirect to the expected page with a cookie which holds the JWT.
Using res.json would only send a json response and would not redirect. That's why the other suggested answer here would not solve the problem I encountered.
So my solution would be:
app.get('/auth/facebook/callback',
passport.authenticate('facebook', {
session: false,
successRedirect : '/',
failureRedirect : '/'
}), (req, res) => {
var token = req.user.jwtoken;
res.cookie('auth', token); // Choose whatever name you'd like for that cookie,
res.redirect('http://localhost:3000'); // OR whatever page you want to redirect to with that cookie
});
After redirection, you can read the cookie safely and use that JWT as expected. (you can actually read the cookie on every page load, to check if a user is logged in)
As I mentioned before, it is possible to redirect with the JWT as a query param, but it's very unsafe.
Using a cookie is safer, and there are still security solutions you can use to make it even safer, unlike a query param which is plainly unsecure.
Adding to Bar's answer.
I prepared a landing component to extract the cookie, save it to local storage, delete the cookie, then redirect to an authorized page.
class SocialAuthRedirect extends Component {
componentWillMount() {
this.props.dispatch(
fbAuthUser(getCookie("auth"), () => {
document.cookie =
"auth=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
this.props.history.push("/profile");
})
);
}
render() {
return <div />;
}
}
A proper solution would be to implement the redirection on the client side.
Simply use:
app.get('/auth/facebook/callback',
passport.authenticate('facebook', {
session: false,
failureRedirect: '/login'
}), (req, res) => {
res.json({
token: req.user.jwtoken
})
}
)
If you're client side receives the token, then redirect from there to home page, and in the case the login wasn't successful, it would be redirected by the server directly.
Or you can go for the full client side management as I would:
app.get('/auth/facebook/callback',
passport.authenticate('facebook', {
session: false
}), (req, res) => {
if (req.user.jwtoken) {
res.json({
success: true,
token: req.user.jwtoken
})
} else {
res.json({
success: false
})
}
}
)
If success === true, store JWT in LocalStorage, else redirect to login page.
I have a slight problem, and it seems to be an easy one, but I cannot seem to wrap my head around what to do.
I have an express app, that uses Firebase to store data. I am able to login, register and log out trough a client side script, but my problem is: How do I check via express if a user is logged in, to be able to send a different page to the logged in users?
This is my code so far:
var firebase = require('firebase');
// Initialize Firebase
var config = {
serviceAccount: "./Chat Application-ad4eaaee3fcc.json",
databaseURL: "MY_DATABASE_URL"
};
firebase.initializeApp(config);
and then I want to show a special page for logged in users, and this is what I have tried:
router.get("/special-page", function(req, res, next) {
var user = firebase.auth().currentUser;
console.log(user); // this variable gets undefined
if(user) {
res.render("special-page");
} else {
res.redirect("/");
}
});
I know this might seem like an easy question, but any help would be much appreciated!
Thanks in advance.
The user side, and server side, are completely different execution areas. Hence, as you probably guessed, calling firebase.auth().currentUser on the server cannot work if the authentication occurred on the client.
The server process just does not have this information, unless the client tells him.
You could just have a request header telling "i am logged as XXX", but it would not be secure, because the server would not be able to verify that information, and a malicious user could pretend to be another one.
The only solution to this, in your use case, is to provide the Firebase token to the server, and then the server needs to verify this token against firebase server, and only then it will be 100% sure about the client authentication.
I needed that in my React app for Server Side Rendering, here is how I did it.
Upon user authentication, set a cookie that contains the firebase token
Unset the cookie when the users logs out
In the server, read the cookie to authenticate client user at each request
Code in the client :
const setAppCookie = () => firebase.auth().currentUser &&
firebase.auth().currentUser.getToken().then(token => {
cookies.set('token', token, {
domain: window.location.hostname,
expire: 1 / 24, // One hour
path: '/',
secure: true // If served over HTTPS
});
});
const unsetAppCookie = () =>
cookies.remove('token', {
domain: window.location.hostname,
path: '/',
});
// triggered by firebase auth changes, this is where you deal
// with your users authentication in your app
fbAuth.onAuthStateChanged(user => {
if (!user) {
// user is logged out
return;
}
// user is logged in
setAppCookie();
// Reset cookie before hour expires
// (firebase tokens are short lived, say the docs)
setInterval(setAppCookie, 3500);
});
[...]
// In the logout code
unsetAppCookie();
Code in the server:
// Before serving express app, enable cookie parsing
app.use(cookieParser());
// In the code dealing with your requests
const { token } = req.cookies;
if (!token) {
// renderWithoutUser();
}
//
// If user found in cookie, verify the token and render with logged in store
//
console.log('Verifying token', token);
firebase.auth().verifyIdToken(token)
.then(decodedToken => {
const uid = decodedToken.sub;
console.log('User is authenticated for this request', uid);
// renderWithUser();
})
.catch(err => {
console.error('WARNING token invalid or user not found', err);
// renderWithoutUser();
});
I'm trying to use this library to authenticate using Linkedin:
https://github.com/auth0/passport-linkedin-oauth2
No Linkedin Login Prompt
I have configured my Passport Linkedin Strategy like so:
var passport = require('passport');
var LinkedInStrategy = require('passport-linkedin-oauth2').Strategy;
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(function(id, done) {
User.findById(id, function (err, user) {
done(err, user);
});
});
passport.use(new LinkedInStrategy({
clientID: 'LINKEDIN_API_KEY',
clientSecret: 'LINKEDIN_API_SECRET',
callbackURL: 'http://localhost:1337/auth/linkedin/callback',
scope: ['r_emailaddress', 'r_basicprofile'],
state: true
}, function(accessToken, refreshToken, profile, done) {
// asynchronous verification, for effect...
process.nextTick(function () {
// To keep the example simple, the user's LinkedIn profile is returned to
// represent the logged-in user. In a typical application, you would want
// to associate the LinkedIn account with a user record in your database,
// and return that user instead.
return done(null, profile);
});
}));
My AuthController.js looks like this:
var passport = require('passport');
module.exports = {
login: function(req, res) {
passport.authenticate('linkedin', function(err, user, info) {
// The request will be redirected to LinkedIn for authentication, so this
// function will not be called.
});
},
callback: function(req, res) {
// ------------------------------------------------------------------------
// after user authenticated, we get the user's email from
// Linkedin's JSON response and save it against the matching
// email address in the User model
// ------------------------------------------------------------------------
console.log(res);
},
logout: function(req, res) {
req.logout();
res.send('logout successful');
}
};
From the linkedin oauth library, I expect the call to:
passport.authenticate('linkedin', function...);
In my AuthController's login action, to redirect the user to Linkedin's login prompt page but what I am actually seeing is my browser just keeps on loading, loading, loading and never stops.
Am I doing something wrong ?
Some questions I am not sure of:
Does Linkedin expect my server to be running on HTTPS before it lets this whole thing starts working ?
Is there some special configurations that I need to do in my Linkedin developers app setting ? (I've enabled all the correct Javascript SDK URLs)
Callback Error
OK, so continuing on, my next problem appears to be here:
return done(null, profile);
^
TypeError: object is not a function
My code is following the npm module instruction here: https://www.npmjs.com/package/passport-linkedin-oauth2
Maybe SailsJS has another way of writing it yet again....
Authentication Always Fails
After fixing the callback error as mentioned in my solution below, I decided to keep moving on and see how it goes despite the Linkedin documentation isn't quite matching 100% to what I expect from the NPM library.
My next problem is my authenticated.js policy appears to always fail.
My code is below:
// We use passport to determine if we're authenticated
module.exports = function (req, res, next) {
if(req.authenticated) { // <---- this is the error line
return next();
}
else
{
res.send(401, {
error: 'Nice try buddy. Try logging in with Linkedin first :]'
});
}
};
No Login Prompt Solution
sigh
I think I'm beginning to grasp some of the difference between SailsJS and pure ExpressJS codes.
The problem appears that I was missing this piece of code at the end of my passport.authenticate() method:
(req, res)
I picked it up after looking this tutorial again: http://iliketomatoes.com/implement-passport-js-authentication-with-sails-js-0-10-2/
So now, the final authenticate method should look like:
passport.authenticate('linkedin', function(err, user, info) {
// The request will be redirected to LinkedIn for authentication, so this
// function will not be called.
})(req, res); // <--- notice this extra (req, res) code here
Which matches the Passportjs documentation:
passport.authenticate('local'),
function(req, res) {
// If this function gets called, authentication was successful.
// `req.user` contains the authenticated user.
res.redirect('/users/' + req.user.username);
});
In a way....if you know what I mean... :D
Now I got my Linkedin login prompt as expected.
Finally!
Callback Error Solution
OK.....I'm not sure if this is completes the login process...but....
I noticed I had an extra line:
passReqToCallback: true
Taken from this page here:
https://github.com/auth0/passport-linkedin-oauth2/issues/29
I removed that and I got a different error message.
I've also changed my callback code to look like:
passport.authenticate('linkedin', function(err, user, info) {
res.json(200, {
user: user
});
})(req, res);
and I got my user JSON which appears to be my Linkedin user profile info:
{
user: {
provider: "linkedin",
...
}
}
But that's...contradicting the Linkedin documentation...I don't see any access_token or expire_in properties which I was expecting to see in step 3 of the Linkedin OAuth 2.0 documentation (https://developer.linkedin.com/docs/oauth2)...
So...supposedly...I should take this user object and create/update against an existing user object ?
Authentication Always Fails Solution
OK, so few more days, I added extra code to generate a User entity if one isn't found in my database, otherwise just return the found user.
The was one last problem, in my policies folder, I have a authenticated.js and it looked like this:
// We use passport to determine if we're authenticated
module.exports = function (req, res, next) {
if(req.authenticated) { // <---- this is the error line
return next();
}
else
{
res.send(401, {
error: 'Nice try buddy. Try logging in with Linkedin first :]'
});
}
};
Being new to all this web development stuff, I thought:
req.authenticated; // should call match name of the file ?
was correct but I was following this tutorial:
http://iliketomatoes.com/implement-passport-js-authentication-with-sails-js-0-10-2/
and he named his file: isAuthenticated.js I figured it's just a name....but I was wrong :D
Turns out, the correct code was:
req.isAuthenticated()
So in full, the correct code becomes:
// We use passport to determine if we're authenticated
module.exports = function (req, res, next) {
if(req.isAuthenticated()) { // alright, that's more like it!
return next();
}
else
{
res.send(401, {
error: 'Nice try buddy. Try logging in with Linkedin first :]'
});
}
};
Perhaps isAuthenticated is a Passportjs function and not just a name like I initially thought.
My further research shows this page which suggests so to me:
Problems getting Passport.js to authenticate user
Maybe req.authenticated can only be used for HTML email-password login form as suggested in above Stackoverflow post and req.isAuthenticated() is for OAuth stuff.
Anyhow, I still don't know if this is the right path but so far, I got authentication in my application now and I can access protected resources. Not sure how long I'll be logged in for, maybe I still need to build the refresh token thingo every 15 minute like the Linkedin documentation stated ?
Hope this helps other fellow Sailsjs users who are facing the same problem :)
Does Linkedin expect my server to be running on HTTPS before it lets
this whole thing starts working ?
No. The API works just as well on a local http setup.
Is there some special configurations that I need to do in my Linkedin
developers app setting ? (I've enabled all the correct Javascript SDK
URLs)
No, your setup is fine.
The browser keeps loading because after the authentication LinkedIn redirects to your callback action which isn't handling the response stream.
You need to handle the response in the callback action. Something like this will do:
callback: function(req, res) {
passport.authenticate('linkedin', function(err, user){
// handle error
// do something with the user (register/login)
return res.redirect('/home');
});
}
I'd highly recommend using sails-generate-auth for maintaining third-party logins. Very easy to setup and configure. All you need to do is serve the access tokens and secrets for the different strategies (either through config/passport.js or, preferably, through config/local.js). Will spare you a lot of redundant code.
I have a question regarding the proper way to logout a user when using passport-saml for authentication.
The example script with passport-saml shows logging out as this:
app.get('/logout', function(req, res){
req.logout();
res.redirect('/');
});
From what I can tell, this will end the local passport session, but it doesn't seem to send a logout request to the SAML IdP. When the user does another login, it redirects to the IdP but immediately redirects back with the authenticated user. Is there a way to logout with the IdP so that the user has to enter their password again when signing in to my site? I've seen other sites that use our IdP do this, so I think it's possible.
I did notice in the passport-saml code that there is a logout() method on the passport-saml Strategy object, which doesn't seem to be called by req.logout(). So I tried switching the code to this:
app.get('/logout', function(req, res) {
//strategy is a ref to passport-saml Strategy instance
strategy.logout(req, function(){
req.logout();
res.redirect('/');
});
});
But I got this error deep in XMLNode.js
Error: Could not create any elements with: [object Object]
at XMLElement.module.exports.XMLNode.element (/.../node_modules/passport-saml/node_modules/xmlbuilder/lib/XMLNode.js:74:15)
at XMLElement.module.exports.XMLNode.element (/.../node_modules/passport-saml/node_modules/xmlbuilder/lib/XMLNode.js:54:25)
at XMLElement.module.exports.XMLNode.element (/.../node_modules/passport-saml/node_modules/xmlbuilder/lib/XMLNode.js:54:25)
at new XMLBuilder (/.../node_modules/passport-saml/node_modules/xmlbuilder/lib/XMLBuilder.js:27:19)
at Object.module.exports.create (/.../node_modules/passport-saml/node_modules/xmlbuilder/lib/index.js:11:12)
at SAML.generateLogoutRequest (/.../node_modules/passport-saml/lib/passport-saml/saml.js:169:21)
Am I not calling this method correctly? Or should I not be calling this method directly and calling some other method instead?
I see that in generateLogoutRequest() it is referring to two properties on the req.user that I'm not sure are there:
'saml:NameID' : {
'#Format': req.user.nameIDFormat,
'#text': req.user.nameID
}
If these properties are not there, will that cause this error? If so, I assume that maybe I need to ensure that these properties are added to the user object that is returned from the verify callback function?
Thanks for any help anyone might be able to provide on this.
Yes adding the nameIDFormat and nameID to the user will solve the issue.
To enable the logout you should configure the logoutURL option in your strategy
logoutUrl: 'http://example.org/simplesaml/saml2/idp/SingleLogoutService.php',
The logout method in the strategy does not actually send any request. the callback function is called with the request as parameter.
To launch the logout process :
passport.logoutSaml = function(req, res) {
//Here add the nameID and nameIDFormat to the user if you stored it someplace.
req.user.nameID = req.user.saml.nameID;
req.user.nameIDFormat = req.user.saml.nameIDFormat;
samlStrategy.logout(req, function(err, request){
if(!err){
//redirect to the IdP Logout URL
res.redirect(request);
}
});
};
edit: the nameId and nameIdFormat has to be saved somewhere on successful login
var samlStrategy = new SamlStrategy(
{
callbackUrl: 'https://mydomain/auth/saml/callback',
entryPoint: 'https://authprovider/endpoint',
logoutUrl: 'https://authprovider/logoutEndPoint',
issuer: 'passport-saml'
},
function(profile, done) {
//Here save the nameId and nameIDFormat somewhere
user.saml = {};
user.saml.nameID = profile.nameID;
user.saml.nameIDFormat = profile.nameIDFormat;
//Do save
});
});
You will also have to create an end point for the logout callback :
This URL should be configured in your SP metadata in your IdP configuration. The IdP will redirect to that URL once the logout is done.
in your routes :
app.post('/auth/saml/logout/callback', passport.logoutSamlCallback);
In your passport configuration :
passport.logoutSamlCallback = function(req, res){
req.logout();
res.redirect('/');
}