Software Engineering & Digital Products for Global Enterprises since 2006
CMMi Level 3SOC 2ISO 27001
Menu
View all services
Staff Augmentation
Embed senior engineers in your team within weeks.
Dedicated Teams
A ring-fenced squad with PM, leads, and engineers.
Build-Operate-Transfer
We hire, run, and transfer the team to you.
Contract-to-Hire
Try the talent. Convert when you're ready.
ForceHQ
Skill testing, interviews and ranking — powered by AI.
RoboRingo
Build, deploy and monitor voice agents without code.
MailGovern
Policy, retention and compliance for enterprise email.
Vishing
Test and train staff against AI-driven voice attacks.
CyberForceHQ
Continuous, adaptive security training for every team.
IDS Load Balancer
Built for Multi Instance InDesign Server, to distribute jobs.
AutoVAPT.ai
AI agent for continuous, automated vulnerability and penetration testing.
Salesforce + InDesign Connector
Bridge Salesforce data into InDesign to design print catalogues at scale.
View all solutions
Banking, Financial Services & Insurance
Cloud, digital and legacy modernisation across financial entities.
Healthcare
Clinical platforms, patient engagement, and connected medical devices.
Pharma & Life Sciences
Trial systems, regulatory data, and field-force enablement.
Professional Services & Education
Workflow automation, learning platforms, and consulting tooling.
Media & Entertainment
AI video processing, OTT platforms, and content workflows.
Technology & SaaS
Product engineering, integrations, and scale for tech companies.
Retail & eCommerce
Shopify, print catalogues, web-to-print, and order automation.
View all industries
Blog
Engineering notes, opinions, and field reports.
Case Studies
How clients shipped — outcomes, stack, lessons.
White Papers
Deep-dives on AI, talent models, and platforms.
Portfolio
Selected work across industries.
View all resources
About Us
Who we are, our story, and what drives us.
Co-Innovation
How we partner to build new products together.
Careers
Open roles and what it's like to work here.
News
Press, announcements, and industry updates.
Leadership
The people steering MetaDesign.
Locations
Gurugram, Brisbane, Detroit and beyond.
Contact Us
Talk to sales, hiring, or partnerships.
Request TalentStart a Project
Web Development

Implementing Authentication and Authorization in Node.js Applications

AG
Amit Gupta
Technical Content Lead
May 11, 2023
10 min read
Implementing Authentication and Authorization in Node.js Applications — Web Development | MetaDesign Solutions

Authentication vs. Authorization: Understanding the Distinction

Authentication answers the question "Who are you?" while authorization answers "What are you allowed to do?" In Node.js applications, these are two distinct layers that must be implemented separately. A user might successfully authenticate (prove their identity with a password) but still be denied access to an admin dashboard because their role does not include the required permissions. Conflating these two concepts is one of the most common security mistakes in backend development, leading to either overly permissive systems or broken access controls.

Username/Password Authentication with Passport.js

Passport.js is the de facto authentication middleware for Node.js, supporting over 500 authentication strategies. The Local Strategy handles traditional username/password login. Install `passport` and `passport-local`, then configure a strategy that queries your database with `User.findOne({ email })` and verifies the password using `bcrypt.compare()`. On successful authentication, Passport serializes the user into the session via `passport.serializeUser()`. Protect routes with `passport.authenticate('local')` middleware. Always hash passwords using `bcrypt` with a salt factor of 12+ before storing them in the database.

Stateless Authentication with JSON Web Tokens (JWT)

For modern API-first architectures, JWT-based authentication eliminates server-side session storage entirely. After successful login, the server generates a token using `jwt.sign({ userId, role }, process.env.JWT_SECRET, { expiresIn: '1h' })`. The client stores this token (typically in an HttpOnly cookie or memory—never localStorage for sensitive tokens) and includes it in the `Authorization: Bearer ` header on subsequent requests. Server middleware verifies the token with `jwt.verify()`, extracting the user payload without any database lookup. This stateless model scales horizontally across multiple server instances effortlessly.

Implementing Refresh Token Rotation for Secure Sessions

Short-lived access tokens (15 minutes) combined with longer-lived refresh tokens (7–30 days) provide the best balance of security and usability. When the access token expires, the client sends the refresh token to a dedicated `/auth/refresh` endpoint. The server verifies the refresh token, invalidates it (one-time use), generates a new access-refresh token pair, and returns both. This refresh token rotation pattern ensures that if a refresh token is ever stolen, it can only be used once before being invalidated, limiting the attack window. Store refresh tokens in the database with a family identifier to detect token reuse attacks.

OAuth 2.0 Social Login with Google, GitHub, and Microsoft

OAuth 2.0 allows users to authenticate using their existing accounts on trusted providers. Using `passport-google-oauth20`, configure the strategy with your Google Client ID, Client Secret, and callback URL. The flow works as follows: user clicks "Login with Google" → redirect to Google's consent screen → Google redirects back to your callback URL with an authorization code → your server exchanges the code for an access token → your server calls Google's API to get the user's profile → `User.findOrCreate()` links the Google profile to your local user record. This same pattern applies to GitHub, Microsoft, Facebook, and any OIDC-compliant provider.

Transform Your Publishing Workflow

Our experts can help you build scalable, API-driven publishing systems tailored to your business.

Book a free consultation

Role-Based Access Control (RBAC) Middleware

RBAC assigns permissions to roles, and roles to users. Define a permissions map: `{ admin: ['users:read', 'users:write', 'posts:delete'], editor: ['posts:read', 'posts:write'], viewer: ['posts:read'] }`. Create a reusable middleware factory: `const authorize = (permission) => (req, res, next) => { if (!req.user) return res.status(401).json({ error: 'Unauthenticated' }); if (!permissions[req.user.role]?.includes(permission)) return res.status(403).json({ error: 'Forbidden' }); next(); }`. Apply it to routes: `router.delete('/posts/:id', authorize('posts:delete'), deletePost)`. This pattern scales cleanly as your application's permission model grows.

Security Hardening: Rate Limiting, CORS, and Helmet

Authentication endpoints are prime targets for brute-force attacks. Use `express-rate-limit` to cap login attempts to 5 per 15-minute window per IP. Configure CORS with `cors({ origin: 'https://yourfrontend.com', credentials: true })` to restrict cookie-based authentication to your own domains. Add `helmet()` middleware to set security headers including `X-Content-Type-Options`, `Strict-Transport-Security`, and `X-Frame-Options`. For JWTs, never store secrets in code—use environment variables. Consider using RS256 (asymmetric) signing for JWTs in microservice architectures where multiple services need to verify tokens without sharing a secret.

Testing Authentication Flows with Supertest and Jest

Authentication logic must be thoroughly tested. Use Supertest with Jest to write integration tests that exercise the full HTTP flow. Test the happy path (valid credentials return a 200 and a token), invalid credentials (return 401), expired tokens (return 403), insufficient permissions (return 403), and rate-limited requests (return 429). Create a test helper that logs in a test user and returns the token for use in subsequent authenticated requests. Mock external OAuth providers using `nock` to intercept HTTP calls to Google/GitHub during CI pipeline runs. Ensure your test database is seeded with users of different roles to validate the RBAC middleware.

FAQ

Frequently Asked Questions

Common questions about this topic, answered by our engineering team.

Authentication verifies WHO a user is (login with password, OAuth, etc.). Authorization determines WHAT that authenticated user is allowed to do (access admin panel, delete posts, etc.). They are two separate middleware layers in a Node.js application.

Use session-based authentication (Passport + express-session) for traditional server-rendered web apps. Use JWTs for API-first architectures, mobile backends, and microservices where stateless, horizontally scalable authentication is required. Combine short-lived access tokens with refresh token rotation for security.

Issue a short-lived access token (15 min) and a longer-lived refresh token (7-30 days). Store refresh tokens in the database. When the access token expires, the client sends the refresh token to a /auth/refresh endpoint. The server verifies it, invalidates it, and issues a new pair.

RBAC (Role-Based Access Control) assigns permissions to roles and roles to users. Create a middleware factory that checks if req.user.role has the required permission. Apply it to routes: router.delete("/posts/:id", authorize("posts:delete"), handler). This scales cleanly as permissions grow.

Use express-rate-limit to cap login attempts per IP (e.g., 5 per 15 minutes). Add helmet() for security headers, configure CORS to restrict cookie-based auth to your domains, and never store JWT secrets in code—use environment variables and consider RS256 asymmetric signing.

Discussion

Join the Conversation

Ready when you are

Let's build something great together.

A 30-minute call with a principal engineer. We'll listen, sketch, and tell you whether we're the right partner — even if the answer is no.

Talk to a strategist
Need help with your project? Let's talk.
Book a call