Web Security Fundamentals: Modern Authentication & Authorisation
A first in our subscriber only series on Web Security Fundamentals. Let's explore the difference between Authentication and Authorisation, and
This is the first entry to our Web Security Fundamentals series. It’s free to all.
Access our Security Cheat Sheet by becoming a free subscriber.
Access the rest of the course by becoming a paid subscriber
Imagine walking into a luxury hotel. At check-in, you show your ID and receive a key card—a small piece of plastic that proves you're a legitimate guest and controls which areas you can access. Now imagine discovering that any guest could reprogram their key card to access any room in the hotel, including yours.
Frightening? This exact scenario just played out with the web browser, Arc. In early 2024, a security researcher discovered a vulnerability in the Arc browser (CVE-2024-45489) that allowed attackers to inject malicious code into any user's browser. What’s wild is it wasn't through some complex zero-day exploit—it was a simple misconfiguration in authentication and authorisation rules.
Let's see how this happened, and more importantly, how to prevent it in your applications.
When Good Authentication Meets Bad Authorisation
Here's what a typical authentication setup looks like in many applications:
// Types for our authentication
interface User {
id: string
username: string
role: string
}
interface AuthRequest extends Request {
user?: User
}
// Authentication middleware
const authenticate = async (
req: AuthRequest,
res: Response,
next: NextFunction,
) => {
const token = req.headers.authorisation?.split(' ')[1]
if (!token) {
return res.status(401).json({ error: 'No token provided' })
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET!)
req.user = decoded as User
next()
} catch (err) {
res.status(401).json({ error: 'Invalid token' })
}
}
// Vulnerable resource access
app.patch('/api/resources/:id', authenticate, async (req, res) => {
const resource = await updateResource(req.params.id, req.body)
res.json(resource)
})
See the problem? While we're checking if the user is authenticated (they have a valid key card), we're not checking if they're authorised to modify this specific resource (it's their room).
This is exactly what happened with Arc. Users were properly authenticated, but could modify other users' data. In hotel terms, every guest could reprogram their key to access any room.
Want to check your code for similar vulnerabilities?
Get our free Authentication Security Cheat Sheet 👉 here.
Adding Basic Protection
Let's fix the most glaring issue:
// Authorisation middleware
const canModifyResource = async (
req: AuthRequest,
res: Response,
next: NextFunction,
) => {
const resourceId = req.params.id
const resource = await getResource(resourceId)
if (!resource) {
return res.status(404).json({ error: 'Resource not found' })
}
// Check if the authenticated user owns this resource
if (resource.ownerId !== req.user?.id) {
return res.status(403).json({
error: 'You can only modify your own resources',
})
}
next()
}
// Protected route with proper authorisation
app.patch(
'/api/resources/:id',
authenticate,
canModifyResource,
async (req: AuthRequest, res: Response) => {
const resource = await updateResource(req.params.id, req.body)
res.json(resource)
},
)
This basic fix helps, but there's more to consider:
What about resource sharing?
How do we handle group permissions?
What about cascading permissions?
How do we manage role hierarchies?
The Bigger Picture
The Arc vulnerability highlights a crucial point: authentication and authorisation are two sides of the same coin. Think back to our hotel:
Authentication: Proving you're a guest (showing ID)
Authorisation: Controlling what you can access (key card permissions)
Here's what commonly goes wrong:
Missing Authorisation Checks
// Vulnerable - only checks authentication
app.get('/api/users/:id/data', authenticate, async (req, res) => {
const userData = await getUserData(req.params.id)
res.json(userData)
})
Implicit Trust of Client Data
// Vulnerable - trusts client-provided owner ID
app.post('/api/resources', authenticate, async (req, res) => {
const resource = await createResource({
...req.body,
ownerId: req.body.ownerId, // DANGEROUS!
})
res.json(resource)
})
Insufficient Role Validation
// Vulnerable - basic role check without a role hierarchy
const isAdmin = (req: AuthRequest, res: Response, next: NextFunction) => {
if (req.user?.role === 'admin') {
next()
} else {
res.status(403).json({ error: 'Admin only' })
}
}
Each of these patterns can lead to serious security vulnerabilities. But fixing them requires understanding:
Proper permission models
Role hierarchies
Resource ownership patterns
Access control lists
Security boundaries
Get your free Authentication Security Cheat Sheet 👉 here to start securing your applications today.
Where to From Here?
We've seen how even major applications can get authentication wrong. The difference between good and bad authentication isn't just in the code—it's in understanding the underlying security principles.
In the upcoming posts in this series, we'll dive deep into:
Building robust authentication systems
Implementing proper authorisation checks
Managing user sessions securely
Handling roles and permissions
Preventing common attack vectors
Ready to build more secure applications? Here's how to get started:
Download our free Authentication Security Cheat Sheet
Subscribe to the complete series to master secure authentication - from basic protection to advanced security patterns
Remember, security is another feature of your platform. Without a correctly built authentication and authorisation layer, you’re leaving yourself open to exploitation.
Want to check your code for similar vulnerabilities?
Get our free Authentication Security Cheat Sheet 👉 here.

