Table of Contents
- Understanding Authentication vs. Authorization
- Authentication Strategies in TypeScript
- Authorization Strategies in TypeScript
- Best Practices for Secure TypeScript Apps
- Essential Tools & Libraries
- Conclusion
- References
1. Understanding Authentication vs. Authorization
Before diving into implementation, it’s critical to distinguish between authentication and authorization—two terms often used interchangeably but with distinct roles:
-
Authentication: The process of verifying the identity of a user or entity (e.g., “Who are you?”). It ensures that the user is who they claim to be (e.g., via passwords, biometrics, or tokens).
-
Authorization: The process of determining if an authenticated user has permission to access a resource or perform an action (e.g., “What can you do?”). It enforces rules like “only admins can delete data” or “users can only edit their own profiles.”
Think of it this way: Authentication is showing your ID to enter a building, while authorization is checking if your ID grants access to specific floors.
2. Authentication Strategies in TypeScript
Authentication is the first line of defense. Below are the most common strategies, along with TypeScript implementation examples.
2.1 JSON Web Tokens (JWT)
JWT is a stateless, compact method for securely transmitting claims between parties. It’s ideal for distributed systems (e.g., microservices) where server-side session storage is impractical.
How JWT Works:
- A user logs in with credentials (e.g., email/password).
- The server validates credentials and issues a JWT (signed with a secret/key).
- The client stores the JWT (e.g., in
localStorageor an HttpOnly cookie). - For subsequent requests, the client sends the JWT in the
Authorizationheader (Bearer <token>). - The server verifies the JWT signature and extracts user data to authenticate the request.
JWT Structure:
A JWT has three parts (base64url-encoded and separated by dots):
- Header: Algorithm (e.g.,
HS256for HMAC-SHA256) and token type. - Payload: Claims (user ID, roles, expiration time
exp). - Signature: Ensures the token hasn’t been tampered with.
TypeScript Implementation with jsonwebtoken:
First, install the library:
npm install jsonwebtoken @types/jsonwebtoken
Example: Issuing and Verifying JWT
import jwt from "jsonwebtoken";
// Secret key (store in environment variables!)
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";
// Payload (avoid sensitive data; JWT is decoded, not encrypted!)
interface UserPayload {
userId: string;
role: "admin" | "user";
}
// Issue a JWT
const issueJWT = (user: UserPayload): string => {
const expiresIn = "1h"; // Token expires in 1 hour
return jwt.sign(user, JWT_SECRET, { expiresIn });
};
// Verify a JWT
const verifyJWT = (token: string): UserPayload => {
try {
return jwt.verify(token, JWT_SECRET) as UserPayload;
} catch (error) {
throw new Error("Invalid or expired token");
}
};
// Usage
const user: UserPayload = { userId: "123", role: "user" };
const token = issueJWT(user);
console.log("Token:", token);
const decoded = verifyJWT(token);
console.log("Decoded user:", decoded); // { userId: "123", role: "user", iat: ..., exp: ... }
Security Considerations for JWT:
- Store tokens securely: Avoid
localStorage(vulnerable to XSS). Use HttpOnly, Secure cookies instead. - Set short expiration times: Use refresh tokens for longer sessions.
- Sign with strong algorithms: Prefer
RS256(asymmetric) overHS256(symmetric) for distributed systems. - Never include sensitive data: JWT payloads are decoded easily; only include non-sensitive claims.
2.2 OAuth 2.0 & OpenID Connect
OAuth 2.0 is a framework for third-party authentication (e.g., “Sign in with Google” or “Sign in with GitHub”). OpenID Connect (OIDC) builds on OAuth 2.0 to add identity verification (via an id_token).
Key OAuth 2.0 Flows:
- Authorization Code Flow: Most secure for server-side apps (e.g., TypeScript backends).
- Implicit Flow: For client-side apps (avoid due to security risks; use PKCE instead).
TypeScript Implementation with passport.js:
passport.js simplifies OAuth integration. Let’s implement “Sign in with Google” using the passport-google-oauth20 strategy.
Step 1: Install dependencies
npm install passport passport-google-oauth20 express @types/passport @types/passport-google-oauth20
Step 2: Configure Google OAuth
import express from "express";
import passport from "passport";
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
const app = express();
// Configure Google OAuth strategy
passport.use(
new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
callbackURL: "/auth/google/callback",
},
(accessToken, refreshToken, profile, done) => {
// Validate/Create user in your database
const user = {
id: profile.id,
email: profile.emails?.[0].value,
name: profile.displayName,
};
return done(null, user);
}
)
);
// Routes
app.get(
"/auth/google",
passport.authenticate("google", { scope: ["profile", "email"] })
);
app.get(
"/auth/google/callback",
passport.authenticate("google", { failureRedirect: "/login" }),
(req, res) => {
// Successful auth: redirect to dashboard
res.redirect("/dashboard");
}
);
app.listen(3000, () => console.log("Server running on port 3000"));
OIDC Note:
For identity verification, OIDC provides an id_token (a JWT) containing user claims (e.g., sub for user ID). Use libraries like jose to verify id_token signatures.
2.3 Session-Based Authentication
Session-based auth relies on server-side storage of user sessions. It’s simpler than JWT for monolithic apps but less scalable for distributed systems.
How It Works:
- User logs in; server creates a session (stored in memory/database/Redis).
- Server sends a session ID via a cookie to the client.
- On subsequent requests, the client sends the cookie; server validates the session ID.
TypeScript Implementation with express-session:
Step 1: Install dependencies
npm install express-session @types/express-session connect-redis redis @types/redis
Step 2: Configure session with Redis (for scalability)
import express from "express";
import session from "express-session";
import { createClient } from "redis";
import connectRedis from "connect-redis";
const app = express();
const RedisStore = connectRedis(session);
// Redis client
const redisClient = createClient({
url: process.env.REDIS_URL || "redis://localhost:6379",
});
redisClient.connect().catch(console.error);
// Session middleware
app.use(
session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET || "your-secret",
resave: false, // Avoid resaving unchanged sessions
saveUninitialized: false, // Don't save unauthenticated sessions
cookie: {
secure: process.env.NODE_ENV === "production", // HTTPS only in prod
httpOnly: true, // Prevent client-side JS access
maxAge: 24 * 60 * 60 * 1000, // 1 day expiration
},
})
);
// Login route
app.post("/login", (req, res) => {
const { email, password } = req.body;
// Validate credentials (e.g., check against database)
const user = { id: "123", email: "[email protected]" };
req.session.user = user; // Attach user to session
res.send("Logged in successfully");
});
// Protected route
app.get("/profile", (req, res) => {
if (!req.session.user) {
return res.status(401).send("Unauthorized");
}
res.send(`Welcome, ${req.session.user.email}`);
});
app.listen(3000, () => console.log("Server running on port 3000"));
Pros/Cons of Session-Based Auth:
- Pros: Server retains control (invalidate sessions instantly), easier to implement for monoliths.
- Cons: Less scalable (requires shared session storage for microservices), higher server resource usage.
3. Authorization Strategies in TypeScript
Once users are authenticated, authorization ensures they only access allowed resources. Below are common strategies.
Role-Based Access Control (RBAC)
RBAC is the most widely used authorization model. It assigns roles (e.g., admin, editor, viewer) to users, and permissions are tied to roles.
Example: RBAC Middleware in TypeScript
import { Request, Response, NextFunction } from "express";
// Define roles and permissions
type Role = "admin" | "editor" | "viewer";
type Permission = "create" | "read" | "update" | "delete";
// Map roles to permissions
const rolePermissions: Record<Role, Permission[]> = {
admin: ["create", "read", "update", "delete"],
editor: ["create", "read", "update"],
viewer: ["read"],
};
// Middleware to check if user has required permission
const requirePermission = (permission: Permission) => {
return (req: Request, res: Response, next: NextFunction) => {
const userRole = req.session.user?.role as Role; // From authentication
if (!userRole || !rolePermissions[userRole].includes(permission)) {
return res.status(403).send("Forbidden: Insufficient permissions");
}
next();
};
};
// Usage in routes
app.post("/posts", requirePermission("create"), (req, res) => {
res.send("Post created successfully");
});
app.delete("/posts/:id", requirePermission("delete"), (req, res) => {
res.send("Post deleted successfully");
});
Best Practices for RBAC:
- Keep roles granular: Avoid overly broad roles (e.g., split
adminintouser-adminandcontent-admin). - Audit roles: Regularly review role assignments to prevent privilege creep.
3.2 Attribute-Based Access Control (ABAC)
ABAC is dynamic and context-aware, using attributes (e.g., user location, time, resource owner) to make authorization decisions. For example:
- “Users can only edit their own posts.”
- “Access to financial data is blocked outside business hours.”
TypeScript Implementation with casl:
casl is a flexible ABAC library for TypeScript. Let’s define rules for a blog app.
Step 1: Install casl
npm install @casl/ability
Step 2: Define ABAC rules
import { Ability, AbilityBuilder } from "@casl/ability";
// Define actions and subjects
type Action = "read" | "create" | "update" | "delete";
type Subject = "Post" | "Comment" | "User";
// Build ability based on user attributes
const defineAbilityFor = (user: { id: string; isAdmin: boolean }) => {
const { can, cannot, build } = new AbilityBuilder(Ability);
// Admins can do anything
if (user.isAdmin) {
can("manage", "all"); // "manage" = all actions
} else {
// Users can read all posts
can("read", "Post");
// Users can create posts
can("create", "Post");
// Users can update/delete their own posts
can(["update", "delete"], "Post", { authorId: user.id });
}
return build();
};
// Usage
const user = { id: "123", isAdmin: false };
const ability = defineAbilityFor(user);
// Check permissions
const post = { id: "post1", authorId: "123" };
console.log(ability.can("update", post)); // true (user is author)
console.log(ability.can("delete", { id: "post2", authorId: "456" })); // false
3.3 Policy-Based Access Control (PBAC)
PBAC uses policies (human-readable rules) to enforce authorization. Policies are stored separately from code, making them easier to update. For example:
- Policy:
allow { role == "manager" && department == "finance" }
Tools for PBAC:
- Open Policy Agent (OPA): A popular open-source policy engine. Integrate OPA with TypeScript by sending authorization requests to an OPA server.
Example: OPA Integration
import axios from "axios";
// Check policy with OPA
const checkPolicy = async (user: any, action: string, resource: any) => {
const response = await axios.post("http://localhost:8181/v1/data/app/authz", {
input: { user, action, resource },
});
return response.data.result.allow;
};
// Usage
const user = { role: "manager", department: "finance" };
const action = "view";
const resource = { type: "salary" };
const allowed = await checkPolicy(user, action, resource);
console.log("Allowed:", allowed); // true (if policy permits)
4. Best Practices for Secure TypeScript Apps
-
Input Validation: Use
zodorjoito validate user input (TypeScript is compile-time, not runtime).import { z } from "zod"; const UserSchema = z.object({ email: z.string().email(), password: z.string().min(8), }); const validateUser = (data: any) => UserSchema.parse(data); // Throws error if invalid -
Secure Headers: Use
helmetto set HTTP headers (e.g.,Content-Security-Policy,X-XSS-Protection).import helmet from "helmet"; app.use(helmet()); -
Rate Limiting: Prevent brute-force attacks with
express-rate-limit.import rateLimit from "express-rate-limit"; const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // 5 attempts per window }); app.post("/login", loginLimiter, (req, res) => { /* ... */ }); -
CORS Configuration: Restrict cross-origin requests to trusted domains.
import cors from "cors"; app.use(cors({ origin: "https://your-frontend.com" }));
5. Essential Tools & Libraries
| Tool/Library | Use Case |
|---|---|
jsonwebtoken | JWT generation/verification |
passport.js | OAuth/OpenID Connect integration |
express-session | Session management |
casl | ABAC implementation |
Open Policy Agent (OPA) | PBAC policy engine |
zod/joi | Runtime input validation |
helmet | Security headers |
express-rate-limit | Rate limiting |
6. Conclusion
Securing TypeScript applications requires a layered approach: strong authentication to verify users, robust authorization to control access, and adherence to best practices like input validation and secure headers.
Choose authentication strategies based on your architecture (JWT for stateless systems, sessions for monoliths, OAuth for third-party logins). For authorization, start with RBAC for simplicity, then adopt ABAC/PBAC for dynamic requirements.
By combining TypeScript’s type safety with these strategies, you’ll build applications that are both scalable and secure.