A login form next to a token icon, representing JWT authentication in a MERN app.

JWT authentication in a MERN app: build a login route that works

Codemithra Team

Codemithra Team

To add login and protected routes to a MERN project, you do not need a full authentication framework. You need three working pieces, and this guide builds each one with real code.

  • A User model that hashes every password with bcryptjs before MongoDB stores it.
  • A login route that checks the password and signs a JSON Web Token.
  • A middleware function that verifies the token before a protected route runs.

The guide also covers one decision most tutorials skip: where the token should live in the browser. It stops short of refresh-token rotation and social login. Role-based permissions are out of scope too. Each of those builds on the same foundation described here.

What a JWT actually contains

A JSON Web Token has three parts. They are joined by dots and encoded in Base64URL. On the wire it looks like xxxxx.yyyyy.zzzzz. The three parts are a header, a payload, a signature (jwt.io).

The header names the token type and the signing algorithm, for example {"alg":"HS256","typ":"JWT"}. The payload carries claims. Registered claims include exp for expiry, iss for issuer, sub for subject, aud for audience (jwt.io).

Base64URL encoding is not encryption. Anyone holding the token can decode the payload and read it. A token must never carry a password, an API key or anything else meant to stay private (jwt.io).

You can sign a JWT with a shared HMAC secret, such as HS256. Or you can sign it with an RSA or ECDSA key pair (jwt.io). This guide uses HS256, with a secret in an environment variable, which suits a single backend service.

jwt.io calls a JWT a stateless mechanism only "in certain cases", not always. Treat that as something you design for, not something you get for free (jwt.io).

The User model

Start with a Mongoose schema for the user. Hash the password before you save it, using a pre('save') hook and the bcryptjs package.

const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const userSchema = new mongoose.Schema({
  email: { type: String, required: true, unique: true, lowercase: true, trim: true },
  password: { type: String, required: true, minlength: 8 },
});

userSchema.pre('save', async function hashPassword(next) {
  if (!this.isModified('password')) return next();
  this.password = await bcrypt.hash(this.password, 10);
  next();
});

module.exports = mongoose.model('User', userSchema);

Two details here matter more than they look. First, always pass the rounds argument to the async bcrypt.hash() call, as the code above does. Without it, bcrypt.hash() throws an error instead of falling back to a default. The default of 10 rounds documented in the bcryptjs README applies to hashSync() and genSalt(), not to the async hash() used here (bcrypt.js README). bcryptjs is a pure JavaScript implementation. That is slower than a native binding, but it installs without a compiler toolchain, which suits a first project.

Second, unique: true on the email path is not a validator. The Mongoose documentation calls it "a convenient helper for building MongoDB unique indexes" (Mongoose validation docs). A duplicate email fails as a MongoDB driver error, not a ValidationError. Your register route needs to catch both error shapes on their own terms, shown next.

Register and login routes

The register route below relies on the schema above to hash the password. It then handles the two failure modes separately.

router.post('/register', async (req, res) => {
  try {
    const user = await User.create({ email: req.body.email, password: req.body.password });
    res.status(201).json({ id: user._id, email: user.email });
  } catch (err) {
    if (err.code === 11000) {
      return res.status(409).json({ error: 'An account with this email already exists.' });
    }
    if (err.name === 'ValidationError') {
      return res.status(400).json({ error: err.message });
    }
    res.status(500).json({ error: 'Registration failed.' });
  }
});

A duplicate key in MongoDB carries code: 11000. A failed Mongoose validator carries name: 'ValidationError'. Those are two different error shapes, so the route checks each one on its own (Mongoose validation docs).

Login compares the submitted password against the stored hash. On a match, it signs a token with an explicit expiry.

router.post('/login', async (req, res) => {
  const user = await User.findOne({ email: req.body.email });
  if (!user) return res.status(401).json({ error: 'Invalid email or password.' });

  const passwordMatches = await bcrypt.compare(req.body.password, user.password);
  if (!passwordMatches) return res.status(401).json({ error: 'Invalid email or password.' });

  const token = jwt.sign(
    { sub: user._id.toString() },
    process.env.JWT_SECRET,
    { algorithm: 'HS256', expiresIn: '1h' }
  );

  res
    .cookie('token', token, { httpOnly: true, secure: true, sameSite: 'strict' })
    .status(200)
    .json({ email: user.email });
});

jsonwebtoken defaults to HS256 when no algorithm is set (node-jsonwebtoken README). Naming it here is a habit worth keeping, not a strict requirement in this one call. expiresIn takes either a number of seconds or a time-span string. A plain JavaScript number is always parsed as seconds. A numeric string with no unit, such as '120', is parsed as milliseconds instead. Always give it an explicit unit, such as '1h', to avoid that trap.

The JWT_SECRET itself needs real entropy. OWASP recommends generating an HMAC secret with a secure generator, with at least 160 bits of entropy, at least as long as the HMAC output (OWASP JWT Cheat Sheet). That rules out a short or memorable string.

Issuing the token safely

The login route above sets the token as a cookie. It does not return the token in the JSON body. That is a deliberate choice, not a style preference.

OWASP's session management guidance is clear on this point. Tokens should not sit in localStorage or sessionStorage, because any script running on the page can read them. One cross-site scripting bug then exposes every stored token (OWASP Session Management Cheat Sheet).

The same guidance recommends a cookie with three flags set: HttpOnly, Secure, SameSite=Strict. HttpOnly blocks page JavaScript from reading the cookie through document.cookie. Secure requires HTTPS transport (OWASP Session Management Cheat Sheet).

Secure only protects the token in transit. It does nothing against a weak secret, a predictable session or tampering on the client, so treat it as one control among several (OWASP Session Management Cheat Sheet).

A cookie also needs to reach a different origin, such as a React app on one port talking to an API on another during development. For that, the server needs a specific origin and credentials: true in its cors configuration. The client request needs credentials enabled too. The cors package documentation shows this exact pairing, and notes that a wildcard origin cannot combine with credentials (Express cors middleware docs).

app.use(cors({
  origin: 'https://your-frontend.example.com',
  credentials: true,
}));

Be precise about what cors actually does here. The middleware sets response headers that a browser enforces. It does not block a request on the server itself, and a non-browser client such as curl ignores it entirely (Express cors middleware docs). Adding cors() is what lets a legitimate browser send the cookie back. It is not a security boundary on its own.

Protecting a route

A protected route needs a middleware function. It reads the cookie, verifies it, then either continues to the handler or stops with an error response.

function authMiddleware(req, res, next) {
  const token = req.cookies.token;
  if (!token) return res.status(401).json({ error: 'Not authenticated.' });

  jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'] }, (err, payload) => {
    if (err) {
      if (err.name === 'TokenExpiredError') {
        return res.status(401).json({ error: 'Session expired, please log in again.' });
      }
      if (err.name === 'JsonWebTokenError') {
        return res.status(401).json({ error: 'Invalid token.' });
      }
      return res.status(401).json({ error: 'Not authenticated.' });
    }
    req.userId = payload.sub;
    next();
  });
}

app.get('/api/profile', authMiddleware, (req, res) => {
  res.json({ userId: req.userId });
});

jwt.verify throws distinct error types for distinct problems. TokenExpiredError carries an expiredAt field. JsonWebTokenError covers a malformed token or a signature that does not match. NotBeforeError covers a token used before its valid time (node-jsonwebtoken README). Branching on these names gives a clearer message to the user, and a clearer log entry for you.

The algorithms: ['HS256'] option is not decoration. It hardcodes which algorithm the server will accept. The token's own header never picks the algorithm at verify time.

Seven step flow from login request to a verified JWT reaching a protected route handler.
The full path from a login request to a verified token reaching the route handler.

Where should the token live?

Storage location is the decision most tutorials skip. It also decides how much damage a single bug can do. The table below compares four options against OWASP's session management guidance.

Storage Readable by page JavaScript Sent automatically cross-site Survives a refresh Effort to build
localStorage or sessionStorage Yes, any script on the page No Yes Low
httpOnly cookie, no Secure or SameSite No Yes, to any origin Yes Low
httpOnly, Secure, SameSite=Strict cookie No No, same-site only Yes Medium
In-memory token plus httpOnly refresh cookie No (refresh cookie is httpOnly) No, same-site only for the refresh cookie No, the token clears, the refresh cookie renews it Higher

OWASP's guidance rules out the first row for a real session. A script reading localStorage can send the token elsewhere in one line, if the page has any cross-site scripting flaw (OWASP Session Management Cheat Sheet). The third row matches OWASP's stated recommendation, and it is what the login route earlier in this guide implements (OWASP Session Management Cheat Sheet). The fourth row adds more moving parts. Choose it only when the app also needs a long browser session without a long-lived access token sitting in the browser.

Common mistakes that break this in production

A handful of mistakes account for most JWT bugs that survive a demo but fail in production.

  • Trusting the algorithm in the token's own header. Some libraries have accepted a header claiming alg: none. Mixing a public-key algorithm with secret-key verification is a separate, known confusion attack. Hardcode the accepted algorithm at verify time, as the middleware above does (OWASP JWT Cheat Sheet).
  • A short or guessable secret. A string like "secret" or a project name does not meet the entropy a real HMAC secret needs (OWASP JWT Cheat Sheet).
  • Forgetting credentials: true on both sides. The server option and the client request both need it. Miss either one, and the cookie never crosses the origin boundary (Express cors middleware docs).
  • Returning the token in the JSON body "for now". Once a token reaches the response body, the next step is often storing it in localStorage for convenience. That is the exact pattern OWASP advises against (OWASP Session Management Cheat Sheet).

An exercise to try today

Take the middleware above and break it three ways on purpose. Send a request with no cookie. Send one with an expired token, by setting expiresIn: '1s' during testing. Send one with the cookie value edited by a single character.

Confirm each case returns its own specific message, not one interchangeable 401. This is the fastest way to find a route where every failure currently looks the same.

Where this fits at Ethnus training

This login flow sits at the point in a MERN curriculum where the Node.js, Express.js and MongoDB modules meet in one working feature. Ethnus's MERN Full Stack program lists named modules for each layer: MongoDB topics on CRUD, aggregation, indexing, Express.js topics on middleware and request handling, and Node.js topics on the runtime and its module system. The program page describes trainer-led sessions with step-by-step walkthroughs and doubt clearing. That kind of support helps when a ValidationError and a duplicate-key error need telling apart for the first time.

Frequently asked questions

Is a JWT encrypted?

No. The header and payload are Base64URL-encoded, not encrypted. Anyone holding the token can decode and read them. Only the signature stops undetected tampering, which is why nothing secret belongs in the payload (jwt.io).

Can I just store the JWT in localStorage to keep things simple?

You can, but OWASP advises against it. Any script on the page can read a token stored there, so one cross-site scripting bug exposes every session. An httpOnly cookie keeps the token out of reach of page JavaScript (OWASP Session Management Cheat Sheet).

Why does my duplicate email registration return a 500 instead of a clear error?

Because unique: true in Mongoose builds an index, not a validator. A duplicate email throws a MongoDB error with code: 11000, not a ValidationError. Catch that code on its own, as shown in the register route above (Mongoose validation docs).

Does adding cors() to my Express app secure my API?

No. The cors middleware sets headers that browsers enforce. It does not block requests on the server itself, so a non-browser client ignores it entirely. It only controls which browser origins can read a cross-origin response (Express cors middleware docs).

What is the difference between jwt.sign and jwt.verify failing?

jwt.sign creates and signs a new token. jwt.verify checks an incoming token and throws a specific error type. Your middleware should catch types such as TokenExpiredError or JsonWebTokenError and respond to each on its own (node-jsonwebtoken README).

The next step is to build this against a real MongoDB instance, not in isolation. Ethnus's MERN Full Stack program gives you trainer support for the parts that behave differently once you connect everything together.

About the Author

Read More

Ethnus User Agreement

I agree to submit my personally identifiable information to Ethnus, who may use it to communicate regarding their events, courses, and other services through various media including phone calls, text messages, email, and social media. I also agree with Ethnus' Privacy Policy and Terms of Service.

I agree with Ethnus sharing my personal data, including email address, with Salesforce family of companies, who may contact me for sales and marketing purposes and as described in Salesforce's Privacy Statement.

Privacy Policy

This Privacy Notice describes how we collect and use your personal information in relation to Ethnus websites, applications, products, services, events, and experiences that reference this Privacy Notice (together, "Ethnus Offerings").

This Privacy Notice does not apply to the "content" processed, stored, or hosted by our customers using Ethnus Offerings in connection with an Ethnus account. This Privacy Notice also does not apply to any products, services, websites, or content that are offered by third parties or have their own privacy notice.

Personal Information We Collect

We collect your personal information in the course of providing Ethnus Offerings to you.

Here are the types of information we gather:

        a) Information You Give Us: We collect any information you provide in relation to Ethnus Offerings. Click here to see examples of information you give us. Example: Name, email, phone, etc.

        b) Automatic Information: We automatically collect certain types of information when you interact with Ethnus Offerings. Example: IP address, location, browser identity, etc.

        c) Information from Other Sources: We might collect information about you from other sources, including service providers, partners, and publicly available sources. Example: marketing analytics, keywords, etc.

How We Use Personal Information

We use your personal information to operate, provide, and improve Ethnus Offerings. Our purposes for using personal information include:

        a) Provide Ethnus Offerings: We may use your personal information to provide and deliver Ethnus Offerings and process transactions related to Ethnus Offerings, including registrations, subscriptions, purchases, and payments.

        b) Measure, Support, and Improve Ethnus Offerings: We use your personal information to measure use of, analyze the performance of, fix errors in, provide support for, improve, and develop Ethnus Offerings.

        c) Recommendations and Personalization: We use your personal information to recommend Ethnus Offerings that might be of interest to you, identify your preferences, and personalize your experience with Ethnus Offerings.

        d) Comply with Legal Obligations: In certain cases, we have a legal obligation to collect, use, or retain your personal information.

        e) Communicate with You: We use your personal information to communicate with you in relation to Ethnus Offerings via different channels (e.g., by phone, email, chat) and to respond to your requests.

        f) Marketing: We use your personal information to market and promote Ethnus Offerings. We might display interest-based ads for Ethnus Offerings.

        g) Purposes for Which We Seek Your Consent: We may also ask for your consent to use your personal information for a specific purpose that we communicate to you.

Cookies

To enable our systems to recognize your browser or device and to provide Ethnus Offerings, we use cookies.

How We Share Personal Information

Information about our customers is an important part of our business and we are not in the business of selling our customers' personal information to others. We share personal information only as described below and with Ethnus Consultancy Services Private Limited, . and its affiliates that are either subject to this Privacy Notice or follow practices at least as protective as those described in this Privacy Notice.

Transactions Involving Third Parties: We make available to you services, software, training, and content provided by third parties for use on or through Ethnus Offerings. You can tell when a third party is involved in your transactions, and we share information related to those transactions with that third party. For example, you can order services, software, and content from sellers using the Authorized Training Partner's marketplace and we provide those sellers information to facilitate your subscription, purchases, or support.

Other than as set out above, you will receive notice when personal information about you might be shared with third parties, and you will have an opportunity to choose not to share the information.

How We Secure Information

        a) We protect the security of your information during transmission to or from websites, applications, products, or services by using encryption protocols and software.

        b) We maintain physical, electronic, and procedural safeguards in connection with the collection, storage, and disclosure of personal information.

Internet Advertising and Third Parties

Ethnus Offerings may include third-party advertising and links to other websites and applications. Third party advertising partners may collect information about you when you interact with their content, advertising, or services. For more information about third-party advertising, including interest-based ads, please read our Interest-Based Ads notice.

Access and Choice

You have choices about the collection and use of your personal information. Many Ethnus Offerings include settings that provide you with options as to how your information is being used. You can choose not to provide certain information, but then you might not be able to take advantage of certain Ethnus Offerings.

        a) Communications: If you do not want to receive promotional messages from us, please unsubscribe or adjust your communication preferences in the emails.

        b) Advertising: If you don't want to see interest-based ads, please adjust your Advertising Preferences.

        c) Browser and Devices: The Help feature on most browsers and devices will tell you how to prevent your browser or device from accepting new cookies, how to have the browser notify you when you receive a new cookie, or how to disable cookies altogether.

Children's Personal Information

We don't provide Ethnus Offerings for purchase by children. If you're under 18, you may use Ethnus Offerings only with the involvement of a parent or guardian.

Retention of Personal Information

We keep your personal information to enable your continued use of Ethnus Offerings, for as long as it is required in order to fulfill the relevant purposes described in this Privacy Notice, as may be required by law (including for tax and accounting purposes), or as otherwise communicated to you. How long we retain specific personal information varies depending on the purpose for its use, and we may delete your personal information in accordance with applicable law.

Contacts, Notices, and Revisions

If you have any concern about privacy at Ethnus, you may also contact us at the addresses below:

Ethnus Consultancy Services Pvt Ltd,

SST Chambers, No.151/17/1 Second Floor, 36th Cross Rd, 5th Block, Jayanagar, Bengaluru, Karnataka 560041

Or, email us at [email protected]

Or call us at: +91 - 8929 334 324

You will find the updated contact information on our website: www.ethnus.com/contact/

If you interact with Ethnus Offerings on behalf of or through your organization, then your personal information may also be subject to your organization's privacy practices, and you should direct privacy inquiries to your organization.

Our business changes constantly, and our Privacy Notice may also change. You should check our website frequently to see recent changes. You can see the date on which the latest version of this Privacy Notice was posted. Unless stated otherwise, our current Privacy Notice applies to all personal information we have about you and your account. We stand behind the promises we make, however, and will never materially change our policies and practices to make them less protective of personal information collected in the past without informing affected customers and giving them a choice.

Terms & Conditions

This Privacy and Security Policy is provided for the benefit of customers and clients of Ethnus Consultancy Services Private Limited. ("Ethnus") as well as other consumers and parties who use Ethnus and/or its website(s), particularly codemithra.com ("Website", "www.codemithra.com", "Codemithra" or "Ethnus Codemithra"), and/or applications ("Apps") (collectively, "Ethnus Services" or "Ethnus Platform").

Since Ethnus serves several different audiences, customers find it helpful to read the Terms of Use that apply specifically to them based upon the purpose for which they use Ethnus. For this reason, we link to three separate agreements below for employer customers, job seeker customers, and staffing customers, respectively.

For your convenience, we define each of these audiences that Ethnus serves as follows:

"Employer Customer" means an entity using Ethnus Services that is seeking to hire an individual as an employee and/or independent contractor to be employed by it directly.

"Job Seeker Customer" means an individual using Ethnus Services who is seeking to be employed as an employee or independent contractor by an employer.

"Staffing Customer" means a staffing company using Ethnus Services that provides staffing services to their own Staffing Clients.

So long as your use of the Ethnus website and services remains within the scope of the particular audience or customer for which you began using Ethnus (e.g. a job seeker does not use Ethnus as an employer, or an employer does not use Ethnus as a job seeker), the complete Terms of Use applicable to your use of the Ethnus website and services is contained within the applicable Terms of Use linked below.

Employer Terms of Use

The following Terms of Use apply to any Ethnus Employer Customer seeking to hire employees or independent contractors for its own business. If you seek to find employees or independent contractors for the benefit of your clients (and not yourself), you need to review the Terms of Use specifically for our Ethnus Staffing Customers accessible at www.Codemithra.com/terms/staffing.

Ethnus, Inc. ("Ethnus") provides online services through which employers and staffing companies seeking employees and independent contractors can efficiently and effectively review and interview candidates. Ethnus provides these services and its suite of features and products through its Apps and Website (collectively, "Ethnus Services") subject to these terms of use ("Terms of Use") and the agreements incorporated herein.

Your privacy is very important to us. We designed our accompanying Privacy and Security Policy to provide important disclosures about how your information will be used by Ethnus in providing you Ethnus Services. These Terms of Use expressly incorporate our Privacy and Security Policy.

Please read these Terms of Use and our Privacy and Security Policy carefully before using any of the diverse Ethnus Services. By visiting the Website, installing any of the Apps, and/or using any of the Ethnus Services, you shall have affirmed your agreement to these Terms of Use.

1. Definitions

2. Modifications - Will Ethnus ever modify these Terms of Use?

3. Ethnus Services - What are the Ethnus Services?

4. Video Content and Services - How and when do you record videos?

5. Pricing, Payments, and Billing - How and when will I be billed for Ethnus Services?

6. Objectionable Content - What if I find content to be objectionable?

7. Customer Conduct

8. Intellectual Property

9. DMCA Policy

10. Reserved for Future Use

11. Resale of Services

12. Indemnification

13. Disclaimer of Warranties

14. Third Party Links and Products

15. Limitations of Liability

16. Exclusions and Limitations

17. General Terms

1. Definitions

"Consumer" means any individual or entity that uses any of the Ethnus Services. Where applicable, the term "Consumer" shall encompass all Ethnus Customers.

"Content" means all material, whether publicly posted or privately transmitted, available on or through any of the Ethnus Services.

"Customer" means, for purposes of this Terms of Use, You, a Job Seeker Customer.

"Customer Content" means any Content uploaded to and/or created through the Ethnus Services by a Ethnus Customer.

"Employer Customer" means an entity using Ethnus Services that is seeking to hire an individual as an employee and/or independent contractor to be employed by it directly.

"GDPR" means the European Union's General Data Protection Regulation.

"Job Seeker Customer" means an individual using Ethnus Services who is seeking to be employed as an employee or independent contractor by an employer.

"Profile Video" means a promotional video created by a Job Seeker Customer to promote themselves as a candidate employee and/or independent contractor. It is not an interview. The Job Seeker Customer completes this independently and on their own.

"Software" means any necessary software used in connection with the Ethnus Services.

"Ethnus Account" means an account associated with a Ethnus Customer who uses or has used Ethnus Services.

"Ethnus Content" means any Content excluding Customer Content and Video Content in which Ethnus does not participate.

"Ethnus Customer" means any person who uses or has used Ethnus Services including, but not limited to, Employer Customers, Job Seeker Customers, and Staffing Customers.

"Ethnus Services" means the suite of features, products and services offered through Ethnus, its Apps, its App Services, the Website, and the Website Services.

"Ethnus Trademarks" means any trademarks, tradenames, logos, and other commercial designs of Ethnus or licensed to Ethnus, whether or not formal registration exists including, but not limited to, "Ethnus."

"Staffing Clients" means third-party employer clients of Staffing Customers.

"Staffing Customer" means a staffing company using Ethnus Services that provides staffing services to their own Staffing Clients.

"Strategic Partners" means those trusted partners that Ethnus employs, engages, or retains to perform functions and/or provide services on its behalf.

"Sub Accounts" means subsidiary accounts created for or by an Employer Customer or Staffing Customer ("such as a consultant group or employer") under its primary account.

"Username" means the valid email address provided by each Ethnus Customer to be used as their username or login identification.

"Video Content" means any video content created by or associated with any Ethnus Customer accessible on and through Ethnus Services including, but not limited to, Profile Videos, Video Questions, Video Interviews, and Welcome Videos.

"Video Interview" means an interview completed through Ethnus Services using a video or "web" camera that an Employer Customer or Staffing Customer requests a Job Seeker Customer complete. A Video Interview may involve a Job Seeker Customer alone or with other participants from an Employer Customer or Staffing Customer. A Video Interview may be pre-recorded by a Job Seeker in response to questions or occur live at which time it would be recorded.

"Video Question" means a question recorded in video and audio that can be sent to potential employee and independent contractor candidates by an Employer Customer or Staffing Customer.

"Website" means all of the content, information and services (in any format whatsoever) accessible through the World Wide Web at the domain name Codemithra.com.

"Website Services" means the services provided by Ethnus through the website at the domain name Codemithra.com, hire.li, and any of our other websites that may be used from time to time