Authorization in a Nutshell: The History of Authorization
One of the most essential aspects of most service-based applications is the authorization. It’s the key to isolating services based on the specific user. Let’s have a look at the history of web authentication and the evolution of authentication methods. Multiple types of authorization methods exist on the web, let’s have a look at them and learn the mechanism behind the scenes.
- Session & Cookies
- Token
- JWT/OAuth
A modern browser has mostly three types of storage mechanisms, like session, cookie, and localstorage. The main difference between those storages is how they work, the session storage data persists as long as the tab is open. The cookie storage data persists until cleaned and it has a special behavior, it’s able to attach all data with all the HTTP request automatically. Finally, we have the localstorage which is used for storing data in the client’s browser and it persists until cleaned.
Session & Cookies
The earlier web apps used this method to authenticate a user. It’s pretty simple, after successfully validating one user’s email and password backend creates a session with a hashed ID, and sets this session ID to the user’s cookie. Now, when a user sends a request the cookie automatically sends this session id with it and the backend now easily identifies the user from the session id. Let’s implement a simple app for demonstration in NodeJS.
const http = require("http");
const fs = require("fs");
const url = require('url');
const getSessions = () => {
try {
const data = fs.readFileSync("./sessions.json", "utf-8");
return JSON.parse(data);
} catch (error) {
return {};
}
};
const createSession = id => {
const sessionId = crypto.randomUUID();
const allSessions = getSessions();
allSessions[sessionId] = { userId: id };
fs.writeFileSync('./sessions.json', JSON.stringify(allSessions));
return sessionId;
};
const getSession = sessionId => {
const allSessions = getSessions();
return allSessions[sessionId];
};
const server = http.createServer((req, res) => {
const parsedUrl = url.parse(req.url, true);
const setCookie = (name, value) => {
const expires = new Date(Date.now() + 86400000); // 24 hours from now
const cookie = `${name}=${value}; Expires=${expires.toUTCString()}; HttpOnly;`;
// Set the cookie in the response header
res.setHeader("Set-Cookie", cookie);
};
const getCookie = name => {
const cookies = req.headers.cookie ? req.headers.cookie.split("; ") : [];
for (const cookie of cookies) {
const [cookieName, cookieValue] = cookie.split("=");
if (cookieName === name) {
return cookieValue;
}
}
return null;
};
if (parsedUrl.pathname === "/login") {
if (getSession(getCookie("user-token"))) {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.parse({ success: true, message: "Already logged in." }));
return;
}
const {email, password} = parsedUrl.query;
if (email !== "[email protected]" || password !== "pass") {
res.writeHead(401, { "Content-Type": "application/json" });
res.end(
JSON.stringify({ success: false, message: "Invalid email or password." })
);
return;
}
const sessionId = createSession(1);
setCookie("user-token", sessionId);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ success: true, message: "Login successful" }));
return;
}
if (parsedUrl.pathname === "/dashboard") {
const sessionUser = getSession(getCookie("user-token"));
if (!authUser) {
res.writeHead(401, { "Content-Type": "application/json" });
res.end(
JSON.stringify({ success: false, message: "Not authenticated." })
);
return;
}
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify({ success: true, message: `The user's id is ${authUser.userId}` })
);
return;
}
// Set the response headers for 404 Not Found
res.writeHead(404, { "Content-Type": "application/json" });
// Send the "404 Not Found" response
res.end(JSON.stringify({ status: false, message: "Not found" }));
});
// Set the server to listen on port 3000
const port = 3000;
server.listen(port, () => {
console.log(`Server running at http://localhost:${port}/`);
});
Token
Session & cookie’s approach has some limitations like users have to log in whenever they clean the browser cookies, and there is very limited control over this approach. But, we could achieve this by using tokens. It’s also very simple instead of creating a session ID and setting it in the client’s cookie we create a hashed token store it on the database with an expiration time and provide this token to the user. Now the user can manually use this token for making their request, what we need for microservices.
const http = require("http");
const fs = require("fs");
const url = require("url");
const getTokens = () => {
try {
const data = fs.readFileSync("./sessions.json", "utf-8");
return JSON.parse(data);
} catch (error) {
return {};
}
};
const createToken = id => {
const tokenId = crypto.randomUUID();
const tokens = getTokens();
tokens[tokenId] = { userId: id };
fs.writeFileSync("./sessions.json", JSON.stringify(tokens));
return tokenId;
};
const getToken = tokenId => {
const tokens = getTokens();
return tokens[tokenId];
};
const server = http.createServer((req, res) => {
const parsedUrl = url.parse(req.url, true);
if (parsedUrl.pathname === "/login") {
if (getToken(req.headers["user-token"])) {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ success: true, message: "Already logged in." }));
return;
}
const { email, password } = parsedUrl.query;
if (email !== "[email protected]" || password !== "pass") {
res.writeHead(401, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
success: false,
message: "Invalid email or password."
})
);
return;
}
const token = createToken(1);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify({ success: true, message: "Login successful", token })
);
return;
}
if (parsedUrl.pathname === "/dashboard") {
const authUser = getToken(req.headers["user-token"]);
if (!authUser) {
res.writeHead(401, { "Content-Type": "application/json" });
res.end(
JSON.stringify({ success: false, message: "Not authenticated." })
);
return;
}
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify({ success: true, message: `The user's id is ${authUser.userId}` })
);
return;
}
// Set the response headers for 404 Not Found
res.writeHead(404, { "Content-Type": "application/json" });
// Send the "404 Not Found" response
res.end(JSON.stringify({ status: false, message: "Not found" }));
});
// Set the server to listen on port 3000
const port = 3000;
server.listen(port, () => {
console.log(`Server running at http://localhost:${port}/`);
});
You may be laughing at this time, it’s the same thing instead of setting the token into the client’s cookie we are returning the token to the user, and accepting the token by header. It’s that simple :D.
JWT/OAuth
It’s a very interesting method. In JWT we don’t have any states to manage, instead, it takes a different approach to generate and validate a JWT token. It deserves a separate tutorial on its own. Let’s break it into another post. Thanks for reading with patience.