Authentication
Authentication Principles
Authentication is the process of verifying a user’s identity, ensuring they are who they claim to be. In web applications, this typically involves users providing a username and password, which the system validates. Once validated, the user is considered “authenticated” and can access protected resources.
Basic Authentication
HTTP Basic Authentication is a simple mechanism that uses the Authorization header, part of the HTTP standard. When a client attempts to access a protected resource, the server sends a 401 Unauthorized response, prompting the client to provide credentials. The client then includes an Authorization header in the format Basic base64(username:password).
Implementation Steps
- Server Requests Authentication: When a client tries to access a protected resource, the server returns a
401 Unauthorizedresponse with aWWW-Authenticateheader indicating the required authentication scheme. - Client Sends Credentials: Upon receiving the
401response, the client includes encoded username and password in theAuthorizationheader of subsequent requests. - Server Validates Credentials: The server decodes the
Authorizationheader, verifies the username and password, and grants access if valid; otherwise, it returns another401response.
Code Implementation
We will use Node.js with the Express framework to implement HTTP Basic Authentication.
Step 1: Set Up Express Application
Create a basic Express application.
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Welcome to the public area.');
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});Step 2: Add Protected Route
Add a protected route that requires authentication.
app.get('/protected', (req, res) => {
if (req.headers.authorization && req.headers.authorization.startsWith('Basic ')) {
const authHeader = req.headers.authorization.split(' ')[1];
const decodedAuthHeader = Buffer.from(authHeader, 'base64').toString('ascii');
const [username, password] = decodedAuthHeader.split(':');
if (username === 'admin' && password === 'secret') {
res.send('Welcome to the protected area.');
} else {
res.status(401).send('Unauthorized');
}
} else {
res.status(401).send('Unauthorized');
}
});Step 3: Send 401 Response
To comply with HTTP Basic Authentication standards, return a 401 Unauthorized response with a WWW-Authenticate header when credentials are missing or incorrect.
app.get('/protected', (req, res) => {
if (!req.headers.authorization || !req.headers.authorization.startsWith('Basic ')) {
res.set('WWW-Authenticate', 'Basic realm="Protected Area"');
res.status(401).send('Unauthorized');
return;
}
const authHeader = req.headers.authorization.split(' ')[1];
const decodedAuthHeader = Buffer.from(authHeader, 'base64').toString('ascii');
const [username, password] = decodedAuthHeader.split(':');
if (username === 'admin' && password === 'secret') {
res.send('Welcome to the protected area.');
} else {
res.status(401).send('Unauthorized');
}
});Step 4: Test with Postman or Browser
Use Postman or a browser’s developer tools to send a GET request to /protected, including an Authorization header with the value Basic YWRtaW46c2VjcmV0 (Base64 encoding of admin:secret).
Security and Limitations
While HTTP Basic Authentication is simple, it has significant security limitations:
- Transmission Security: Credentials are sent in plaintext, so HTTPS is required to prevent man-in-the-middle attacks.
- Persistence Issues: It does not support persistent sessions, requiring credentials to be sent with every request.
Form-Based Authentication
Form-based authentication involves users entering a username and password in an HTML form, which is submitted to the server for validation. If valid, the server creates a session and sends a session identifier (usually a cookie) to the client for subsequent requests to identify the user.
Implementation Steps
- Create a Form: Build a frontend form to collect username and password.
- Handle Form Submission: Set up a backend route to process form submissions.
- Validate User: Verify the provided credentials on the server.
- Create Session: If credentials are valid, create a session and store the session ID in a cookie.
- Protect Routes: Check the session ID in the cookie for protected routes and validate its authenticity.
Code Implementation
Using Express and express-session Middleware
Step 1: Set Up Express and Session Middleware
const express = require('express');
const session = require('express-session');
const app = express();
app.use(session({
secret: 'your-secret-key',
resave: false,
saveUninitialized: true,
}));
app.use(express.urlencoded({ extended: true }));Step 2: Create Login Route
app.post('/login', (req, res) => {
const { username, password } = req.body;
if (username === 'admin' && password === 'password') {
req.session.authenticated = true;
res.redirect('/dashboard');
} else {
res.status(401).send('Invalid credentials');
}
});Step 3: Protect Routes
app.get('/dashboard', (req, res) => {
if (req.session.authenticated) {
res.send('Welcome to your dashboard');
} else {
res.redirect('/login');
}
});JSON Web Tokens (JWT)
JWT is an open standard (RFC 7519) for securely transmitting information between parties. It consists of three parts: Header, Payload, and Signature. JWT is ideal for stateless environments since all authentication information is contained in the token, eliminating the need for server-side session storage.
JWT Workflow
- Generate Token: After a user logs in, the server generates a JWT and sends it to the client.
- Client Stores Token: The client typically stores the JWT in local storage or a cookie.
- Attach to Requests: The client attaches the JWT to the
Authorizationheader in subsequent requests. - Verify Token: The server verifies the JWT’s signature and payload to confirm its validity.
Stateless Authentication with JWT
Step 1: Install JWT Package
npm install jsonwebtokenStep 2: Create JWT Token
const jwt = require('jsonwebtoken');
app.post('/login', (req, res) => {
const { username, password } = req.body;
if (username === 'admin' && password === 'password') {
const token = jwt.sign({ username }, 'your-secret-key', { expiresIn: '1h' });
res.json({ token });
} else {
res.status(401).send('Invalid credentials');
}
});Step 3: Protect Routes
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (token == null) return res.sendStatus(401);
jwt.verify(token, 'your-secret-key', (err, user) => {
if (err) return res.sendStatus(403);
req.user = user;
next();
});
}
app.get('/dashboard', authenticateToken, (req, res) => {
res.send(`Welcome to your dashboard, ${req.user.username}`);
});OAuth 2.0
OAuth 2.0 is an open standard for authorizing applications to access user resources on another service without sharing credentials. It is primarily used for authorization, not authentication.
OAuth 2.0 Workflow Overview
- Authorization Request: The client application redirects the user to the authorization server’s login page.
- User Authentication: The user logs in and authorizes the client application.
- Authorization Code: The authorization server returns an authorization code to the client.
- Token Request: The client uses the authorization code to request an access token from the authorization server.
- Access Token: The authorization server returns an access token to the client.
- Resource Access: The client uses the access token to access resources on the resource server.
Third-Party Authentication Integration
Integrating OAuth 2.0 in Express typically involves using a library like Passport.js, which provides strategies to simplify OAuth integration.
Step 1: Configure Passport OAuth Strategy
const passport = require('passport');
const GitHubStrategy = require('passport-github').Strategy;
passport.use(new GitHubStrategy({
clientID: 'YOUR_CLIENT_ID',
clientSecret: 'YOUR_CLIENT_SECRET',
callbackURL: 'http://localhost:3000/auth/github/callback'
}, function(accessToken, refreshToken, profile, cb) {
// Query database to find or create user
User.findOrCreate({ githubId: profile.id }, function(err, user) {
return cb(err, user);
});
}));Step 2: Set Up OAuth Login Routes
app.get('/auth/github',
passport.authenticate('github'));
app.get('/auth/github/callback',
passport.authenticate('github', { failureRedirect: '/login' }),
function(req, res) {
// Successful authentication, redirect to homepage
res.redirect('/');
});OpenID Connect
OpenID Connect is an identity authentication layer built on OAuth 2.0, allowing client applications to obtain access tokens and user information, such as name and email address.
Differences Between OpenID Connect and OAuth
- OAuth 2.0: Focuses on authorization, allowing one application to access resources on another.
- OpenID Connect: Adds identity authentication capabilities to OAuth 2.0, enabling applications to verify user identity.
Implementation Example
Using OpenID Connect in Express can be achieved with Passport.js’s OpenID Connect strategy.
Step 1: Configure Passport OpenID Connect Strategy
const OpenIDConnectStrategy = require('passport-openidconnect').Strategy;
passport.use(new OpenIDConnectStrategy({
issuer: 'https://your-op-server.com',
authorizationURL: 'https://your-op-server.com/authorize',
tokenURL: 'https://your-op-server.com/token',
userInfoURL: 'https://your-op-server.com/userinfo',
clientID: 'YOUR_CLIENT_ID',
clientSecret: 'YOUR_CLIENT_SECRET',
callbackURL: 'http://localhost:3000/auth/openid/callback',
scope: ['openid', 'profile', 'email']
}, function(issuer, profile, done) {
User.findOrCreate({ openid: profile.id }, function(err, user) {
return done(err, user);
});
}));Step 2: Set Up OpenID Connect Login Routes
app.get('/auth/openid',
passport.authenticate('openid-connect'));
app.get('/auth/openid/callback',
passport.authenticate('openid-connect', { failureRedirect: '/login' }),
function(req, res) {
// Successful authentication, redirect to homepage
res.redirect('/');
});Multi-Factor Authentication (MFA)
Types of MFA
- Knowledge Factor: Information only the user knows, such as a password or security question answer.
- Possession Factor: Something the user possesses, like a smartphone or hardware token.
- Biometric Factor: A user’s physical characteristic, such as a fingerprint or facial recognition.
Implementing MFA
Implementing MFA in an Express application can involve methods like Time-Based One-Time Passwords (TOTP), as used by Google Authenticator.
Step 1: Generate and Distribute Secret Key
const speakeasy = require('speakeasy');
// Generate a TOTP secret
const secret = speakeasy.generateSecret({ length: 20 });
// Send the secret as a QR code to the user
res.send(`<img src="${secret.otpauth_url}" />`);Step 2: Verify TOTP
app.post('/mfa', (req, res) => {
const code = req.body.code;
const verified = speakeasy.totp.verify({
secret: secret.base32,
encoding: 'base32',
token: code
});
if (verified) {
// MFA verification successful
res.send('MFA verification successful');
} else {
// MFA verification failed
res.status(401).send('MFA verification failed');
}
});Authentication Libraries and Middleware
Passport.js is a popular authentication middleware for Node.js and Express, supporting various authentication strategies, including MFA.
Introduction to Passport.js
Passport.js is a flexible and modular authentication middleware offering strategies for local username/password, OAuth, OpenID Connect, MFA, and more.
Implementing MFA with Passport.js
Assuming basic username/password authentication is implemented, add TOTP-based MFA.
Step 1: Configure Passport.js
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const SpeakeasyStrategy = require('passport-speakeasy').Strategy;
passport.use(new LocalStrategy(
function(username, password, done) {
// Implement local username/password authentication logic
}
));
passport.use(new SpeakeasyStrategy(
function(user, token, done) {
// Implement TOTP verification logic
}
));Step 2: Set Up MFA Routes
app.post('/login', passport.authenticate('local'), (req, res) => {
// After successful username/password validation, redirect to MFA page
res.redirect('/mfa');
});
app.post('/mfa', passport.authenticate('speakeasy'), (req, res) => {
// After successful MFA verification, log in the user
req.login(user, function(err) {
if (err) { return next(err); }
return res.redirect('/');
});
});Authorization
Role-Based Access Control (RBAC)
RBAC Concept
RBAC is an authorization model where users are assigned roles with specific permissions. Each role defines a set of operations, and users access resources based on their roles.
Permission Assignment and Management
In Express, RBAC can be implemented using middleware.
Step 1: Define Roles and Permissions
const roles = {
admin: ['read', 'write', 'delete'],
editor: ['read', 'write'],
viewer: ['read']
};Step 2: Create Authorization Middleware
function authorize(role, resource, action) {
return function(req, res, next) {
if (roles[role].includes(action)) {
next();
} else {
res.status(403).send('Access denied');
}
};
}Step 3: Use Authorization Middleware
app.get('/admin', authorize('admin', 'admin', 'read'), (req, res) => {
res.send('Admin panel');
});Attribute-Based Access Control (ABAC)
How ABAC Works
ABAC is a dynamic authorization model that makes access decisions based on attributes of users, resources, and the environment. This allows decisions to consider factors like time, location, or device type, in addition to user roles.
Advantages of Implementing ABAC
- Flexibility: ABAC supports fine-grained access control, dynamically adjusting permissions based on context.
- Scalability: ABAC expresses authorization rules through attributes, making it easy to extend and modify.
Implementing ABAC
Implement ABAC in Express using a library like casbin, which provides robust ABAC support.
Step 1: Install casbin
npm install casbinStep 2: Define Policy
const Casbin = require('casbin');
async function loadPolicy() {
const e = new Casbin.Enforcer('path/to/model.conf', 'path/to/policy.csv');
await e.loadPolicy();
return e;
}
const enforcer = loadPolicy();Step 3: Check Permissions
app.get('/secure-resource', async (req, res) => {
const userId = req.user.id;
const resourceId = '123';
const action = 'read';
try {
const allowed = await enforcer.enforce(userId, resourceId, action);
if (allowed) {
res.send('Access granted');
} else {
res.status(403).send('Access denied');
}
} catch (error) {
res.status(500).send(error);
}
});Access Control Lists (ACL)
ACL Concept
ACL is a resource-based access control mechanism where each resource has an associated list defining which subjects can perform specific operations on it. ACLs allow fine-grained access control for each resource.
Comparison of ACL and RBAC
- ACL: Resource-based access control, with each resource having an independent access control list specifying who can access it and how.
- RBAC: Role-based access control, where users are assigned roles that define accessible resources and operations.
Permission Checks
Performing Permission Checks Before Route Handling
In Express, permission checks are typically implemented via middleware, intercepting requests before they reach the route handler to verify permissions.
Step 1: Define Permission Check Middleware
function checkPermission(permission) {
return function(req, res, next) {
// Check if the user has the required permission
if (req.user.permissions.includes(permission)) {
next(); // Permission granted, proceed with request
} else {
res.status(403).send('Access Denied'); // Permission denied, reject request
}
};
}Step 2: Use Permission Check Middleware
app.get('/admin', checkPermission('admin-access'), (req, res) => {
res.send('Admin panel');
});Authorization Libraries and Frameworks
Using casbin for Authorization
casbin is a powerful authorization library supporting RBAC, ABAC, and ACL.
Step 1: Install casbin
npm install casbinStep 2: Define Model and Policy
const Casbin = require('casbin');
// Define model
const modelText = `
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
`;
// Create enforcer
const e = new Casbin.Enforcer('model.conf', 'policy.csv');
// Load policy
await e.loadPolicy();Step 3: Check Permissions
app.get('/secure-resource', async (req, res) => {
const userId = req.user.id;
const resourceId = '123';
const action = 'read';
try {
const allowed = await e.enforce(userId, resourceId, action);
if (allowed) {
res.send('Access granted');
} else {
res.status(403).send('Access denied');
}
} catch (error) {
res.status(500).send(error);
}
});Using feathers-authentication for Authorization
feathers-authentication is an authentication and authorization plugin for Feathers.js, also compatible with Express applications.
Step 1: Install feathers-authentication
npm install @feathersjs/authentication-client @feathersjs/authentication-jwtStep 2: Configure feathers-authentication
const feathers = require('@feathersjs/feathers');
const authentication = require('@feathersjs/authentication');
const app = feathers();
app.configure(authentication());Step 3: Use feathers-authentication for Authorization
app.service('users').find().then(data => {
console.log(data);
}).catch(err => {
console.error(err);
});



