If we are talking about a single web project, then information about the state of a particular session of interaction between the client and the server is easy to maintain using user authentication at his login. But if such an independent system evolves, turning into several systems, the developer is faced with the question of maintaining information about the state of each of these separate systems. In practice, this question looks like this: "Will the user of these systems have to enter each of them separately and also exit from them?"
There is one good rule of thumb about systems that grow in complexity over time and how those systems interact with their users. Namely, the burden of solving problems associated with the complication of the project architecture falls on the system, and not on its users. It doesn't matter how complex the internal mechanisms of the web project are. It should look like a unified system to the user. In other words, a user working with a web system consisting of many components should perceive what is happening as if he is working with one system. In particular, we are talking about authentication in such systems using SSO (Single Sign-On) - a single sign-on technology.
How do I create systems that use SSO? You might think of the good old cookie-based solution here, but this solution is subject to limitations. The restrictions apply to the domains from which cookies are installed. It can be circumvented only by collecting all domain names of all subsystems of the web application on one top-level domain.
In today's environment, such solutions are hindered by the widespread adoption of microservice architectures. Session management got more complicated at a time when different technologies were used in developing web projects, and when different services were sometimes hosted on different domains. In addition, web services that used to be written in Java started to write using the capabilities of the Node.js platform. This made it harder to work with cookies. It turned out that sessions are now not so easy to manage.
These difficulties have led to the development of new methods of logging into systems, in particular, we are talking about single sign-on technology.
Single sign-on technology
The basic principle on which the single sign-on technology is based is that a user can log in to one system of a project consisting of many systems and be authorized in all other systems without having to log in again. At the same time, we are talking about a centralized exit from all systems.
We, for educational purposes, are going to implement SSO technology on the Node.js platform.
It should be noted that the implementation of this technology on a corporate scale will require much more effort than we are going to put into the development of our training system. That is why there are specialized SSO solutions designed for large-scale projects.
How is SSO login organized?
At the heart of the SSO implementation is a single, independent authentication server that can accept information to authenticate users. For example - email address, username, password. Other systems do not provide the user with direct mechanisms to log into them. They authorize the user indirectly by receiving information about him from the authentication server. Indirect authorization mechanisms are implemented using tokens.
Here is the code repository for the simple-sso project, the implementation of which I will describe here. I am using the Node.js framework, but you can implement the same using something different. Let's take a step-by-step analysis of the actions of the user working with the system, and the mechanisms that make up this system.
Step 1
The user is trying to access a protected resource on the system (let's call this resource the "SSO consumer", "sso-consumer"). The SSO consumer finds out that the user is not logged in and redirects the user to the "SSO server" ("sso-server") using its own address as a query parameter. A successfully authenticated user will be redirected to this address. This mechanism is provided by Express middleware:
const isAuthenticated = (req, res, next) => {
// , ,
// - SSO-
// URL URL,
// ,
const redirectURL = `${req.protocol}://${req.headers.host}${req.path}`;
if (req.session.user == null) {
return res.redirect(
`http://sso.ankuranand.com:3010/simplesso/login?serviceURL=${redirectURL}`
);
}
next();
};
module.exports = isAuthenticated;
Step 2
The SSO server finds out that the user is not logged in and redirects him to the login page:
const login = (req, res, next) => {
// req.query url,
// , sso-.
//
//
const { serviceURL } = req.query;
// URL.
if (serviceURL != null) {
const url = new URL(serviceURL);
if (alloweOrigin[url.origin] !== true) {
return res
.status(400)
.json({ message: "Your are not allowed to access the sso-server" });
}
}
if (req.session.user != null && serviceURL == null) {
return res.redirect("/");
}
// -
//
if (req.session.user != null && serviceURL != null) {
const url = new URL(serviceURL);
const intrmid = encodedId();
storeApplicationInCache(url.origin, req.session.user, intrmid);
return res.redirect(`${serviceURL}?ssoToken=${intrmid}`);
}
return res.render("login", {
title: "SSO-Server | Login"
});
};
I'll make some comments here regarding security.
We check
serviceURL
, coming in the form of a request parameter to the SSO server. This allows us to find out if this URL is registered in the system and if the service it represents can use the services of an SSO server.
Here's what a list of URLs for services that are allowed to use the SSO server might look like:
const alloweOrigin = {
"http://consumer.ankuranand.in:3020": true,
"http://consumertwo.ankuranand.in:3030": true,
"http://test.tangledvibes.com:3080": true,
"http://blog.tangledvibes.com:3080": fasle,
};
Step 3
The user enters a username and password that are sent to the SSO server in the login request.
Login page
Step 4
The SSO authentication server verifies the user's information and creates a session between itself and the user. This is the so-called "global session". An authorization token is created immediately. The token is a string of random characters. How exactly this string is generated doesn't matter. The main thing is that similar lines are not repeated for different users, and that such a line would be difficult to forge.
Step 5
The SSO server takes the authorization token and passes it to where the newly logged in user came from (that is, it passes the token to the SSO consumer).
const doLogin = (req, res, next) => {
// .
// ,
// userDB - , ,
const { email, password } = req.body;
if (!(userDB[email] && password === userDB[email].password)) {
return res.status(404).json({ message: "Invalid email and password" });
}
//
const { serviceURL } = req.query;
const id = encodedId();
req.session.user = id;
sessionUser[id] = email;
if (serviceURL == null) {
return res.redirect("/");
}
const url = new URL(serviceURL);
const intrmid = encodedId();
storeApplicationInCache(url.origin, id, intrmid);
return res.redirect(`${serviceURL}?ssoToken=${intrmid}`);
};
Again, some security notes:
- This token should always be considered as an intermediate mechanism, it is used to obtain another token.
- If you are using the JWT as an intermediate token, try not to include secrets in it.
Step 6
The SSO consumer receives a token and contacts the SSO server to verify the token. The server checks the token and returns another token with user information. This token is used by the SSO consumer to create a session with the user. This session is called local.
Here is the middleware code used in the Express-based SSO consumer:
const ssoRedirect = () => {
return async function(req, res, next) {
// , req queryParameter, ssoToken,
// , .
const { ssoToken } = req.query;
if (ssoToken != null) {
// ssoToken , .
const redirectURL = url.parse(req.url).pathname;
try {
const response = await axios.get(
`${ssoServerJWTURL}?ssoToken=${ssoToken}`,
{
headers: {
Authorization: "Bearer l1Q7zkOL59cRqWBkQ12ZiGVW2DBL"
}
}
);
const { token } = response.data;
const decoded = await verifyJwtToken(token);
// jwt,
// global-session-id id ,
// .
req.session.user = decoded;
} catch (err) {
return next(err);
}
return res.redirect(`${redirectURL}`);
}
return next();
};
};
After receiving a request from an SSO consumer, the server checks the token for its existence and expiration date. The verified token is considered valid.
In our case, the SSO server, after successful verification of the token, returns a signed JWT with information about the user.
const verifySsoToken = async (req, res, next) => {
const appToken = appTokenFromRequest(req);
const { ssoToken } = req.query;
// ssoToken .
// ssoToken - , .
if (
appToken == null ||
ssoToken == null ||
intrmTokenCache[ssoToken] == null
) {
return res.status(400).json({ message: "badRequest" });
}
// appToken -
const appName = intrmTokenCache[ssoToken][1];
const globalSessionToken = intrmTokenCache[ssoToken][0];
// appToken , SSO-
if (
appToken !== appTokenDB[appName] ||
sessionApp[globalSessionToken][appName] !== true
) {
return res.status(403).json({ message: "Unauthorized" });
}
// ,
const payload = generatePayload(ssoToken);
const token = await genJwtToken(payload);
// ,
delete intrmTokenCache[ssoToken];
return res.status(200).json({ token });
};
Here are some safety notes.
- All applications that will use this server for authentication must be registered with the SSO server. They need to be assigned codes that will be used to verify them when they make requests to the server. This allows for a higher level of security when communicating between the SSO server and SSO consumers.
- It is possible to generate different "private" and "public" rsa files for each application and let each of them verify their JWTs in-house with their respective public keys.
In addition, you can define an application-level security policy and organize its centralized storage:
const userDB = {
"info@ankuranand.com": {
password: "test",
userId: encodedId(), // , .
appPolicy: {
sso_consumer: { role: "admin", shareEmail: true },
simple_sso_consumer: { role: "user", shareEmail: false }
}
}
};
After the user successfully logs into the system, sessions are created between him and the SSO server, as well as between him and each subsystem. The session established between the user and the SSO server is called a global session. A session established between a user and a subsystem that provides the user with some services is called a local session. After the local session is established, the user will be able to work with the subsystem resources closed to extraneous resources.
Setting up local and global sessions
A quick tour of the SSO consumer and SSO server
Let's take a quick tour of the SSO consumer and SSO server functionality.
β SSO Consumer
- The SSO consumer subsystem does not authenticate the user by redirecting the user to the SSO server.
- This subsystem receives the token passed to it by the SSO server.
- It interacts with the server to verify the validity of the token.
- She receives the JWT and validates this token using the public key.
- This subsystem establishes a local session.
βSSO Server
- The SSO server validates user login information.
- The server creates a global session.
- It creates an authorization token.
- An authorization token is sent to the SSO consumer.
- The server verifies the validity of the tokens passed to it by SSO consumers.
- The server sends an SSO JWT to the consumer with user information.
Organization of centralized logout
Similar to how SSO was implemented, you can implement SSO technology. Here you just need to take into account the following considerations:
- If a local session exists, a global session must also exist.
- If a global session exists, it does not necessarily mean that a local session exists.
- If the local session is destroyed, the global session must also be destroyed.
Outcome
As a result, it can be noted that there are many ready-made implementations of single sign-on technology that you can integrate into your system. They all have their own advantages and disadvantages. Developing such a system independently, from scratch, is an iterative process during which you need to analyze the characteristics of each of the systems. This includes login methods, user information storage, data synchronization, and more.
Do your projects use SSO mechanisms?