Protocol Sharing Web App โ Combining Session Authentication, OAuth, and IDOR Protection
After Completing This Topic
By combining the session authentication, OAuth 2.0, and IDOR concepts learned in the textbook, you will be able to build a web application that securely shares experimental protocols. You will understand why real-world tools like protocols.io have sophisticated authorization systems, and how to build them using Python/Node.
This article provides a general educational example. For production deployments, use managed services such as Auth0, Clerk, or Supabase Auth.
"Wait, if I just change the URL, can I see other lab protocols?" โ The Trap of IDOR
Let's say you've created a website for sharing experimental protocols.
- User login: OK
- Create your own protocol: OK
- View a specific protocol:
/api/protocols/42
A friend changes the URL slightly: /api/protocols/43. A secret protocol from another lab is revealed. Simply changing the number in the URL allows access to someone else's data โ this is an IDOR (Insecure Direct Object Reference) vulnerability.
The cause of this problem is interesting. Your code checks if the user is logged in (authentication), but it doesn't check if the user has permission to access this resource (authorization). Authentication and authorization are separate concepts.
This is why A01: Broken Access Control consistently ranks high in the OWASP Top 10. Developers repeatedly create this issue. All real-world services like protocols.io, GitHub, and Notion defend against this vulnerability with sophisticated permission systems.
In the language of computer science, this problem is expressed as an interface lacking an authorization gate. When an API endpoint receives a request, checking who is making the request is authentication, and checking if they have permission to access this specific resource is authorization. If only authentication is present, the system is in a state where anyone who is logged in can access any resource.
From Black Box to Components
Component 1: Session-Based Authentication
The most basic authentication pattern. Upon successful login, the server creates a session and passes the session ID to the browser via a cookie.
import express from "express";
import session from "express-session";
import bcrypt from "bcrypt";
import pg from "pg";
const app = express();
app.use(express.json());
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 24 * 60 * 60 * 1000
}
}));
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
app.post("/api/login", async (req, res) => {
const { email, password } = req.body;
const result = await pool.query("SELECT id, password_hash FROM users WHERE email = $1", [email]);
if (result.rows.length === 0) {
return res.status(401).json({ error: "Invalid credentials" });
}
const user = result.rows[0];
const valid = await bcrypt.compare(password, user.password_hash);
if (!valid) {
return res.status(401).json({ error: "Invalid credentials" });
}
req.session.userId = user.id;
res.json({ ok: true });
});
app.post("/api/logout", (req, res) => {
req.session.destroy(() => res.json({ ok: true }));
});Essential Security Considerations:
httpOnly: true- Prevents JavaScript from accessing cookies (protects against session hijacking via XSS).secure: true- Only transmit over HTTPS (in production).sameSite: "lax"- Provides some protection against CSRF.- Use
bcryptto hash passwords (never store passwords in plain text). - Store the session secret in an environment variable.
Component 2: Authentication Middleware
Middleware used to protect endpoints that should only be accessible to authenticated users.
function requireAuth(req, res, next) {
if (!req.session.userId) {
return res.status(401).json({ error: "Login required" });
}
next();
}
app.get("/api/me", requireAuth, async (req, res) => {
const result = await pool.query("SELECT id, email, lab_id FROM users WHERE id = $1", [req.session.userId]);
res.json(result.rows[0]);
});Component 3: Resource Owner Validation (Preventing IDOR)
Key Principle: When retrieving resources, always include the owner condition in the WHERE clause.
app.get("/api/protocols/:id", requireAuth, async (req, res) => {
const { id } = req.params;
const result = await pool.query(
`SELECT p.*
FROM protocols p
WHERE p.id = $1
AND (
p.owner_user_id = $2
OR p.visibility = 'public'
OR EXISTS (
SELECT 1 FROM protocol_shares ps
WHERE ps.protocol_id = p.id AND ps.user_id = $2
)
)`,
[id, req.session.userId]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: "Not found" });
}
res.json(result.rows[0]);
});Security Principles:
- In addition to
WHERE id = $1, always include the owner condition. - If a user is not the owner and the protocol is not shared, return a 404 (not 403). Avoid exposing that the resource exists but is inaccessible.
- Apply the same principle to delete and update endpoints.
Component 4: OAuth 2.0 Social Login
Use external authentication providers like Google or GitHub instead of email/password login.
import { OAuth2Client } from "google-auth-library";
const oauth = new OAuth2Client({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
redirectUri: process.env.GOOGLE_REDIRECT_URI
});
app.get("/api/auth/google", (req, res) => {
const url = oauth.generateAuthUrl({
access_type: "offline",
scope: ["email", "profile"],
state: generateStateToken(req)
});
res.redirect(url);
});
app.get("/api/auth/google/callback", async (req, res) => {
const { code, state } = req.query;
if (!verifyStateToken(req, state)) {
return res.status(400).json({ error: "Invalid state" });
}
const { tokens } = await oauth.getToken(code);
const ticket = await oauth.verifyIdToken({
idToken: tokens.id_token,
audience: process.env.GOOGLE_CLIENT_ID
});
const payload = ticket.getPayload();
const email = payload.email;
let user = await pool.query("SELECT id FROM users WHERE email = $1", [email]);
if (user.rows.length === 0) {
user = await pool.query(
"INSERT INTO users (email, oauth_provider, oauth_id) VALUES ($1, 'google', $2) RETURNING id",
[email, payload.sub]
);
}
req.session.userId = user.rows[0].id;
res.redirect("/");
});Essential OAuth Security Considerations:
stateparameter: Protects against CSRF. Generate a random token during the initial request, store it in the session, and verify it in the callback.id_tokenverification: Verify the signature using the provider's public key.audienceverification: Confirm that the token is intended for this application.- PKCE: Required for native apps.
Pipeline Assembly
Overall web app flow:
1. User visits /login
2. Email/password login or clicks "Sign in with Google"
3. On success, a session cookie is issued, and the user is redirected to /dashboard
4. /dashboard displays the user's list of protocols
- GET /api/protocols?mine=true (WHERE owner = session.userId)
5. User clicks on a specific protocol
- GET /api/protocols/42 (including owner verification)
6. User adds a user to share with
- POST /api/protocols/42/share (after verifying ownership)Each endpoint must include authentication (who is the user) + authorization (what permissions does the user have).
Fading โ Three Blank Spaces for You to Fill
Blank 1: Audit Logging
Log all sensitive actions. Track who did what and when.
app.use(async (req, res, next) => {
const originalJson = res.json.bind(res);
res.json = function(body) {
// TODO 1: Collect request information (userId, method, path, statusCode)
// TODO 2: Insert into the audit_logs table (asynchronous fire-and-forget)
// TODO 3: Call originalJson
return originalJson(body);
};
next();
});Hint: pool.query("INSERT INTO audit_logs (user_id, method, path, status) VALUES ($1, $2, $3, $4)", [req.session.userId, req.method, req.path, res.statusCode]).catch(console.error);.
Blank 2: Role-Based Access Control (RBAC)
Users have roles (admin, editor, viewer), and each role has different permissions.
function requireRole(role) {
return async (req, res, next) => {
// TODO 1: Query the users table with session.userId
// TODO 2: Verify if the user's role is equal to or greater than the required role
// TODO 3: If not, return 403
next();
};
}
app.delete("/api/protocols/:id", requireAuth, requireRole("admin"), async (req, res) => {
// Only admins can delete
});Blank 3: Session Hijacking Prevention
Invalidate the session if the IP/User-Agent changes, even if the session cookie is stolen.
function detectSessionAnomaly(req, res, next) {
if (!req.session.userId) return next();
const currentIp = req.ip;
const currentUA = req.headers["user-agent"];
// TODO 1: Compare with originalIp and originalUA stored in the session
// TODO 2: If they differ significantly, destroy the session and require re-login
// TODO 3: If normal, proceed to the next middleware
next();
}Hint: On the first login, req.session.originalIp = req.ip; req.session.originalUA = req.headers["user-agent"];. In subsequent requests, if (req.session.originalIp !== req.ip) { req.session.destroy(); return res.status(401).json({error: "Session anomaly"}); }.
Reflection โ Differences from a Production-Ready Authentication System
Managed Services: The vast majority of production systems use managed services like Auth0, Clerk, Supabase Auth, and Firebase Auth. What you've built is an understanding of what these services do behind the scenes.
PASETO / JWT: Self-contained, verifiable tokens instead of sessions. This eliminates the need for a session store, which offers scalability benefits. However, managing revocation (logout) becomes more challenging.
MFA (Multi-Factor Authentication): TOTP (Google Authenticator), WebAuthn (hardware keys). The last line of defense against account compromise.
Zero Trust Architecture: Re-authenticates and re-authorizes every request. It does not rely on network location (e.g., internal network) as a basis for trust.
Password Alternatives: Passkeys (WebAuthn) are the new standard. Passwordless login. Supported by Apple, Google, and Microsoft.
OWASP ASVS: Application Security Verification Standard. A tiered checklist of the security requirements that your application should meet.
Extension Project
1. Passkey Support: Add hardware authentication using the WebAuthn library.
2. Team Workspace: Allow multiple users to belong to the same workspace and share protocols. Invitation links.
3. API Token System: Support programmatic access using API keys instead of sessions. Permissions based on scopes.
4. Rate Limiting: Prevent brute-force attacks. Limit login attempts to 5 per minute per IP address.
Component Guide for This Section
- [F] Session Authentication: Cookies, session storage, httpOnly, secure, sameSite.
- [F] OAuth 2.0: Authorization code flow, state parameter, id_token validation.
- [F] IDOR Prevention: Ownership check required when accessing resources. Choose between 404 vs 403.
- [W] Express & pg: Routing and database access (complete script provided).
[F] = You implement it yourself / [W] = Provided as a complete code.