Date: 2024-12-05
At the bottom of this article, you will find recommendations and implementation basics, what is the authorization and authentication approach to choose in each scenario, and why. We will also look at the pros and cons of various authorization and authentication types and how to make decisions that's best for your application.
The issue of security in the web/cyber security is an enormous and broad topic, I will be going straight to the points I consider useful at all levels.
Adding authentication and authorization for example, to a REST API with user and product inventory routes is crucial for security. The choice of tools and libraries depends on your stack and requirements. But here are industry-standard ways to implement these features:
What is authentication:
Authentication refers to a process or situation where a server needs to know or verify who is accessing its content. Authentication would usually go both ways the client also wants to be sure the server provider is really who they say they are, so while the server is trying to verify that the client is who they say they are so does a legitimate client the server. In a single sentence, Authentication verifies the identity of a user.
Industry-Standard Tools and Libraries
1.JWT (JSON Web Tokens):
Library:
Use Case: Stateless authentication, where the server does not store session data.
How It Works:
token
is generated and signed with a secret or private key
.token
is sent to the client
and included in every request (usually in the Authorization header as Bearer token
).verifies the token to authenticate the user
.Basic Example
* TypeScript:
import jwt from 'jsonwebtoken';
const generateToken = (userId: string) => {
return jwt.sign({ id: userId }, process.env.JWT_SECRET!, { expiresIn: '1h' });
};
const verifyToken = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ message: 'Unauthorized' });
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET!);
req.user = decoded;
next();
} catch (err) {
res.status(403).json({ message: 'Forbidden' });
}
};
2.OAuth2:
Libraries:
Use Case: When integrating with third-party identity providers (e.g., Google, Facebook, GitHub).
How It Works:
Users log in using third-party accounts, and your API validates the access tokens issued by these providers.3.Session-Based Authentication:
Library:
Use Case: When you need server-side session storage for user authentication.
How It Works:
On the other hand, authorization simply means controlling access to resources based on user roles and permissions.
Industry-Standard Tools and Libraries
1.Role-Based Access Control (RBAC):
Libraries:
How It Works:
admin
, user
, manager
).admin
can manage products, user
can view inventory).Example with CASL:
* TypeScript:
import { AbilityBuilder, Ability } from '@casl/ability';
const defineAbilitiesFor = (user) => {
const { can, cannot, build } = new AbilityBuilder(Ability);
if (user.role === 'admin') {
can('manage', 'all'); // Admins can do everything
} else {
can('read', 'Product'); // Regular users can only read products
cannot('delete', 'Product'); // Regular users cannot delete products
}
return build();
};
const checkPermission = (action, resource) => (req, res, next) => {
const ability = defineAbilitiesFor(req.user);
if (ability.can(action, resource)) {
return next();
}
res.status(403).json({ message: 'Forbidden' });
};
// Use in routes
app.delete('/products/:id', checkPermission('delete', 'Product'), (req, res) => {
// Product deletion logic
});
2.Attribute-Based Access Control (ABAC):
Libraries:
How It Works: Policies define access based on user attributes, resource attributes, and environmental context.
For projects where you don’t want to build these systems from scratch, use managed services:
1.Auth0:
2 Firebase Authentication:
3.AWS Cognito:
4.Okta:
In simple terms, Middleware ensures that only authorized users can access protected routes.
Example Middleware for Authentication:
* TypeScript:
import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';
const authenticate = (req: Request, res: Response, next: NextFunction) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ message: 'Unauthorized' });
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET!);
req.user = decoded;
next();
} catch (err) {
res.status(403).json({ message: 'Forbidden' });
}
};
export default authenticate;
5.Best Practices
1.Authentication:
2.Authorization:
3.Protected Routes:
Comparison of session-based authentication and stateless authentication (e.g., JWT), along with recommendations for security-conscious use cases.
Session-based authentication involves storing session data on the server and using cookies to maintain user sessions.
How It Works:
User logs in, and the server creates a session (a unique ID linked to the user) and stores it in a database or in-memory store (e.g., Redis).
The session ID is sent to the client as a cookie.
On subsequent requests, the client includes the cookie, and the server retrieves the session data to authenticate the user.
Pros:
Server-Side Control:
Easier to Implement in Monolithic Applications:
More Secure Against Token Theft
HttpOnly
, Secure
, and SameSite
attributes, they are less likely to be stolen via client-side attacks like XSS.Cons:
Scalability Issues
Tied to Cookies
HttpOnly
, Secure
, SameSite
, etc.) to prevent attacks like CSRF and XSS.Stateful
Stateless authentication uses self-contained tokens, typically in the form of JSON Web Tokens (JWTs), to authenticate users.
How It Works:
User logs in, and the server generates a signed JWT containing claims (e.g., user ID, roles).
The token is sent to the client (usually stored in Authorization
headers or localStorage).
On subsequent requests, the server validates the token's signature without storing any state.
Pros:
Scalability:
Flexibility:
Performance:
Cons:
Token Revocation
Security Concerns with Token Storage
localStorage
exposes them to XSS attacks, while storing them in cookies makes them vulnerable to CSRF attacks (though mitigated with SameSite
cookies).Payload Size
Session-Based Authentication:
Pros for Security:
Centralized session management allows easy invalidation of compromised sessions.
HttpOnly
and Secure
cookies mitigate risks from XSS.
Strong against CSRF when combined with proper cookie attributes and CSRF tokens.
Cons for Security
Stateless Authentication (JWT):
Pros for Security:
Cons for Security:
The best choice depends on your architecture and specific requirements. Here’s a recommendation based on security concerns:
Use Session-Based Authentication When:
Use Stateless Authentication (JWT) When:
You can combine the strengths of both approaches:
Example:
JWT for access: Short-lived token (e.g., 15 minutes) stored in memory or headers.
Refresh token: Long-lived, securely stored (e.g., in an HttpOnly cookie).
Backend logic
Feature | Session-Based Authentication | Stateless Authentication (JWT) |
---|---|---|
Scalability | Limited, requires session store | Highly scalable |
Ease of Implementation | Simpler for monoliths | Simpler for distributed systems |
Security | Stronger control, revocable | Harder to revoke |
Storage | Server-side | Client-side |
Use Case | Banking, enterprise systems | Distributed systems, APIs, microservices |
For security-sensitive use cases, session-based authentication with well-configured cookies is generally safer. For distributed systems, consider a hybrid approach to balance security and scalability.
When combining Passport.js with JWT (stateless authentication), the industry-standard strategy is the passport-jwt
strategy. This approach leverages JSON Web Tokens for authentication while benefiting from Passport's modularity and middleware-based integration.
Why passport-jwt?
1.Stateless Authentication:
2.Compatibility:
3.Security:
4.Flexibility:
passport-jwt
1.Install Required Packages
bash
npm install passport passport-jwt jsonwebtoken
2.Configure the JWT Strategy
import passport from 'passport';
import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt';
// Your secret or private key
const JWT_SECRET = process.env.JWT_SECRET || 'your_secret_key';
// JWT options
const opts = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: JWT_SECRET,
};
// Define the strategy
passport.use(
new JwtStrategy(opts, async (jwtPayload, done) => {
try {
// Example: Validate the user based on the payload
const user = await findUserById(jwtPayload.id); // Replace with your DB call
if (user) {
return done(null, user); // Pass user to req.user
}
return done(null, false); // No user found
} catch (error) {
return done(error, false);
}
})
);
3.Issue a JWT on Login
Use a library like jsonwebtoken
to generate a token when the user logs in.
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET || 'your_secret_key';
function generateToken(user) {
return jwt.sign(
{ id: user.id, username: user.username, role: user.role }, // Payload
JWT_SECRET,
{ expiresIn: '1h' } // Expiration time
);
}
4.Protect Routes Using Middleware
import express from 'express';
const app = express();
// Protect an endpoint
app.get(
'/protected',
passport.authenticate('jwt', { session: false }),
(req, res) => {
res.json({ message: 'You are authorized!', user: req.user });
}
);
passport-jwt
1.Use Short-Lived Tokens:
2.Secure Token Storage:
HttpOnly
cookies for web applications (mitigates XSS attacks).localStorage
or sessionStorage
unless necessary.3.Validate Tokens on Each Request:
4.Token Revocation:
5.Combine with Other Strategies:
Example Hybrid Flow: Combining passport-local
with passport-jwt
1.Install passport-local
bash
npm install passport-local
2.Configure passport-local
TypeScript:
import { Strategy as LocalStrategy } from 'passport-local';
// Example local strategy
passport.use(
new LocalStrategy(async (username, password, done) => {
try {
const user = await findUserByUsername(username); // Replace with your DB call
if (!user || !validatePassword(user, password)) {
return done(null, false, { message: 'Invalid credentials' });
}
return done(null, user);
} catch (error) {
return done(error);
}
})
);
3.Login Route (Issue JWT)
TypeScript:
app.post('/login', (req, res, next) => {
passport.authenticate('local', { session: false }, (err, user, info) => {
if (err || !user) {
return res.status(401).json({ message: info?.message || 'Unauthorized' });
}
// Generate token
const token = generateToken(user);
return res.json({ token });
})(req, res, next);
});
Feature | Session-Based (passport-local) | JWT-Based (passport-jwt)) |
---|---|---|
Statefulness | Requires session store | Stateless |
Scalability | Limited | Highly scalable |
Security | Centralized control | Decentralized, token revocation needed |
Best Use Case | Monolithic apps | Distributed apps, APIs |
Was this page helpful?
If you find this article helpful, please feel free to share the link.
Yes👍 No👎