How to implement JumpCloud SSO to Node.js application using SAML protocol
Hey everyone! In this post, I would like to share with you guys how to implement the SSO
to a Node.js
application with SAML
protocol.
Introduction to SSO and SAML
For those who do not know what a SSO
is, SSO
or Single Sign On is an authentication method which allows user to sign in or log in into multiple applications or systems using the a single set of credentials. SSO
increases the user experience and the most important part is it enhances security by reducing the number of passwords users need to remember and by centralising authentication processes.
SAML
(Security Assertion Markup Language) and OpenID
are the two main protocols for SSO
. OpenID
is notable for its decentralised approach, allowing users to use one set of credentials across various websites. In contrast, SAML
is widely used in enterprise environments, offering robust authentication solutions. Both protocols aim to simplify user authentication, improving both convenience and security in today's digital landscape.
JumpCloud configuration
There is a lot of things that we have to configure in our JumpCloud account. When I was doing this project, it was a bit of frustrating as they were not many examples and the documentation was a bit confusing to me. Hence, I decided to write this post. You can refer my Github repo for the complete code.
Before we can integrate the SSO
to our application, we have to create a custom application in JumpCloud. Simply go to the Admin Console, there is a sidebar on the left. We will see a section named User Authentication
, then click on SSO Application
. Click on Create a new application
and you will be directed to this page:
Now, we select the Custom Application
and this option allows us to integrate our application with SSO. There will be several features available, simply click on Manage Single-Sign-On (SSO)
as in the photo below:
Click Next and then we can add logo for your application or we can leave it as the default. Next click on Saved Application
button.
Now, our application is ready to be integrated. All we have to do is to configure some information in our custom application.
Go to the SSO Applications
tab and click on our created custom application.
Click on the SSO
tab, and here is where we configure everything.
Most of the fields are going to be left as default. The fields that we need to change are:
SP Entity ID
ACS URL
Login URL
Attributes (optional)
SP Entity ID
refers to Service Provider ID
which is unique for the SAML
entity. In short word, SP
is our own application. Hence, the ID
should be our application URL/sso
. For example, http://your-web-url.com/sso
.
ACS URL
stands for Assertion Customer Service URL
which is responsible to send the SAML
response. Here, we set it as https://your-web-url.com/login/callback
.
Login URL
is the path to our login page in our own application.
Attributes are the fields that are sent in our SAML
response. This option can be used to store session for instance. You can put anything for the Service Provider Attribute Name
for this case. Just make sure they are tally with JumpCloud attributes to avoid confusion.
Now, we are done configuring our JumpCloud for our application. Before we leave the console, we need to do another two things; download the IDP certificate
and take note of IDP URL
.
IDP certificate
can be found on the left side of our application settings:
This certificate is important as our Identity Provider (IdP)
which is in this case JumpCloud will use it to verify the signature of the SAML
responses.
The IDP URL is the URL where users will be directed to for the authentication. This is the IdP's SSO URL. We can obtain this URL at the SSO
tab:
Finally we can continue working on our coding.
Create your Node.js
As usual, we create our Node.js
application by using these commands:
npm init -y
We also need to install Express.js
for session authorisation, Passport.js
for authentication and authorisation purposes, fs
to read the certificate.
npm install express passport fs express-session
There are some libraries or packages you might need, but these packages above are explicitly needed for this particular situation. I also would like to highlight that I will not be implementing the JWT
as this application is only to demonstrate how the SSO
works.
Here, we create a config.js
for our JumpCloud configuration where we add all configuration we set on JumpCloud:
const fs = require('fs');
module.exports = {
passport: {
strategy: 'saml',
saml: {
path: 'https://your-web-url.com/login/callback',
entryPoint:
process.env.SAML_ENTRY_POINT ||
'https://sso.jumpcloud.com/saml2/systemname',
issuer: 'passport-saml',
cert: fs.readFileSync('your-jumpcloud-sso-certrificate.cer', 'utf-8')
},
},
};
Next, we create a file named passport.js
. This file is used to configure our Passport.js
library along with SAML
configurations. Block of code below is the example of the configuration:
const SamlStrategy = require('passport-saml').Strategy;
const config = require('./config');
module.exports = function (passport) {
passport.serializeUser(function (user, done) {
done(null, user);
});
passport.deserializeUser(function (user, done) {
done(null, user);
});
passport.use(
new SamlStrategy(config.passport.saml, function (profile, done, req) {
try {
// Access the firstName from the SAML profile
const { uid, email, firstName } = profile;
// The `info` parameter is added to the callback
return done(null, {
id: uid,
email: email,
firstName: firstName,
}, { message: 'Authentication successful', profile: profile });
} catch (error) {
console.error('Error in SAML authentication:', error);
return done(error);
}
})
);
}
Here we configure Passport.js
for SAML
authentication where its strategy and configuration settings, sets up functions to serialise and deserialise user objects, and defines the SAML
authentication strategy. The strategy extracts user information from the SAML
profile, constructs a user object, and passes it to Passport.js
for authentication.
After that, we now can integrate our SSO
using Passport
. We configure routes for handling SSO
authentication, initiating login, and processing the login callback.
We start by importing all the libraries needed as well as the config
and passport
modules we just created:
const express = require('express');
const app = express();
const passport = require('passport');
const PORT = process.env.PORT || 3500;
const config = require('./config');
require('./passport')(passport);
Next, we add middleware where passport.initialize()
to initialise Passport. passport.session()
middleware to support persistent login sessions.
app.use(passport.initialize());
app.use(passport.session());
We add this route to handle SSO
authentication callback. When the SAML
response is posted back to the server, this route processes it passport.authenticate(config.passport.strategy, ...)
where it authenticates the incoming SAML
response. If authentication fails, it redirects to the home page (/
). On successful authentication, it redirects to the home page.
app.post(
config.passport.saml.path,
passport.authenticate(config.passport.strategy, {
failureRedirect: '/',
failureFlash: true,
}),
function (req, res) {
res.redirect('/');
}
);
Then, we initialise the route for SSO
login process. When a GET
request is made to /login
, it triggers the SAML
authentication process.
app.get('/login', (req, res) => {
passport.authenticate('saml', {
failureRedirect: '/login',
failureFlash: true,
})(req, res);
});
At last, we add this route to process our login callback. It handles the callback from the SAML
provider after the user has authenticated. passport.authenticate('saml', ...)
is to process the SAML
response. It logs the authentication information and handles errors. If an error occurs, it logs the error and renders the login page with an error message. If no user is found, it logs the message and renders the login page with an appropriate error. If authentication is successful, req.logIn(user, ...)
logs in the user and establishes a session. It sets the username in the session and redirects to the home page (/home
).
app.post('/login/callback', (req, res, next) => {
passport.authenticate('saml', (err, user, info) => {
try {
console.log('SAML Authentication Info:', info);
if (err) {
console.error('Error in SSO authentication:', err);
return res.render('login', { error: 'SSO authentication failed' });
}
if (!user) {
console.error('User not found');
return res.render('login', { error: 'User not found' });
}
req.logIn(user, function (loginErr) {
if (loginErr) {
console.error('Error logging in:', loginErr);
return res.render('login', { error: 'Login failed' });
}
req.session.user = info?.profile?.username;
return res.redirect('/home');
});
} catch (error) {
console.error('Error in SSO callback:', error);
return res.render('login', { error: 'SSO callback failed' });
}
})(req, res, next);
});
So now, our application will use the JumpCloud SSO. When our user login, they will be directed to this page:
As we can see in our URL tab, it includes SAML
and SSO
. We can also can verify this by using a Chrome Extension named SAML-tracker. It will show SAML
like in the photo below to show that this application is using SAML
protocol:
Once user successfully logs in, they will be directed to this page:
Here is some applications that this user bounds to. This user can go to any of these applications without having to log in twice.
This is an example of SAML response:
SAML Authentication Info: {
message: 'Authentication successful',
profile: {
issuer: 'https://your-web-url.com/sso',
inResponseTo: '_xxxxxxxxxxxxx',
sessionIndex: 'xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx',
nameID: 'example@abc.com',
nameIDFormat: 'urn:oasis:names:tc:SAML:1.0:nameid-format:unspecified',
nameQualifier: undefined,
spNameQualifier: undefined,
email: 'example@abc.com',
username: 'example',
attributes: { email: 'example@abc.com', username: 'example' },
getAssertionXml: [Function (anonymous)],
getAssertion: [Function (anonymous)],
getSamlResponseXml: [Function (anonymous)]
}
}
And, that's it! Now we successfully integrate JumpCloud SSO
with our application. Yeay! 🥳