MEAN Stack

Developing the User Authentication Service in MEAN Stack

By the Domain India teamPublished 8 min read
Knowledge base article
Contents (8 sections)

Sign-up, login and password reset are the first parts of a web app that attackers test. This guide builds an email-and-password authentication service for a MEAN app (MongoDB, Express, Angular, Node.js) the way it should be done in 2026: hashed passwords, a session token in an httpOnly cookie that Angular never touches, rate-limited login, and hashed reset tokens.

Key takeaways

Hash passwords with bcrypt or Argon2id, put the signed session token in an httpOnly, Secure, SameSite cookie with a short lifetime, rate-limit login and reset, and store only a hash of each reset token. In Angular, add an HTTP interceptor that sends requests with credentials, keep the signed-in user in a signal, and protect routes with a functional guard that asks the API who is signed in. Never keep tokens in localStorage.

The Express and MongoDB side in full

The server side of a MEAN app is the same as a MERN app's. The complete, tested code for the user model, register, login, logout, protected routes and password reset is in MERN stack: developing the user authentication service. This page summarises it and covers the Angular side.

1. What we are building

  • POST /api/auth/register, POST /api/auth/login and POST /api/auth/logout
  • GET /api/me, which tells Angular who is signed in
  • POST /api/auth/forgot and POST /api/auth/reset for password reset
  • middleware that protects every other API route

Use a current Node.js LTS line (22 or 24), Express 5 and Mongoose 8. Express 5 passes errors thrown in async handlers to your error handler. On the front end, use a current Angular release with standalone components, signals and functional guards and interceptors.

2. The server in brief

Keep secrets in environment variables: MONGODB_URI, and a JWT_SECRET generated with openssl rand -base64 64. The user model stores a passwordHash (never the password), a tokenVersion number for signing a user out everywhere, and the reset-token fields, with select: false on anything sensitive.

The login route checks the password and sets the session cookie:

javascript
const cookieOptions = { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 60 * 60 * 1000 };

router.post('/login', limiter, async (req, res) => {
  const email = String(req.body.email || '').toLowerCase().trim();
  const user = await User.findOne({ email }).select('+passwordHash');
  const ok = user && await bcrypt.compare(String(req.body.password || ''), user.passwordHash);
  if (!ok) return res.status(401).json({ error: 'Email or password is incorrect.' });

  const token = jwt.sign({ sub: user.id, v: user.tokenVersion }, process.env.JWT_SECRET,
    { algorithm: 'HS256', expiresIn: '1h' });
  res.cookie('session', token, cookieOptions);
  res.json({ id: user.id, email: user.email });
});

The middleware verifies the cookie on every protected route:

javascript
module.exports = async function requireAuth(req, res, next) {
  try {
    const payload = jwt.verify(req.cookies.session || '', process.env.JWT_SECRET,
      { algorithms: ['HS256'] });
    const user = await User.findById(payload.sub);
    if (!user || user.tokenVersion !== payload.v) throw new Error('stale');
    req.user = user;
    next();
  } catch {
    res.status(401).json({ error: 'Please sign in.' });
  }
};

Rules the code follows:

  • One error message for a wrong email or a wrong password, so attackers can't find out which emails have accounts.
  • bcrypt cost 12 or Argon2id for password hashes; never a fast hash such as SHA-256.
  • algorithms passed to jwt.verify, so a token claiming another algorithm is rejected.
  • Rate limiting on login, register, forgot and reset, for example with express-rate-limit.
  • Reset tokens of 32 random bytes, emailed to the user, stored only as a SHA-256 hash with a 30-minute expiry, and cleared after use. Incrementing tokenVersion on reset signs the user out everywhere.

Because the session is an httpOnly cookie, Angular never reads or stores the token. It only has to let the browser send the cookie. A small interceptor does that for every request:

typescript
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors, HttpInterceptorFn } from '@angular/common/http';
import { routes } from './app.routes';

const withCredentials: HttpInterceptorFn = (req, next) =>
  next(req.clone({ withCredentials: true }));

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(withInterceptors([withCredentials])),
  ],
};

4. An auth service with signals

typescript
// auth.service.ts
import { Injectable, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { catchError, of, tap } from 'rxjs';

export interface User { id: string; email: string; }

@Injectable({ providedIn: 'root' })
export class AuthService {
  private http = inject(HttpClient);
  readonly user = signal<User | null>(null);

  login(email: string, password: string) {
    return this.http.post<User>('/api/auth/login', { email, password })
      .pipe(tap(u => this.user.set(u)));
  }

  logout() {
    return this.http.post<void>('/api/auth/logout', {})
      .pipe(tap(() => this.user.set(null)));
  }

  loadCurrentUser() {
    return this.http.get<User>('/api/me').pipe(
      tap(u => this.user.set(u)),
      catchError(() => { this.user.set(null); return of(null); }),
    );
  }
}

Components read auth.user() to show the signed-in state. Show errors from the API as they are; don't add hints such as "no account with that email".

5. Protect routes with a guard

typescript
// auth.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { map } from 'rxjs';
import { AuthService } from './auth.service';

export const authGuard: CanActivateFn = () => {
  const auth = inject(AuthService);
  const router = inject(Router);
  return auth.loadCurrentUser().pipe(
    map(user => user ? true : router.createUrlTree(['/login'])),
  );
};

Add canActivate: [authGuard] to the protected routes. A guard only improves the user experience; the real protection is the requireAuth middleware on the server, because anyone can call your API directly.

6. Same site, CORS and CSRF

Serve the Angular build and the API from the same site, with the app at / and the API at /api/. The cookie then works with SameSite=Lax and you don't need CORS. During development, use the Angular CLI proxy so the dev server forwards /api to Express:

json
{
  "/api": { "target": "http://localhost:3000", "secure": false }
}

Save it as proxy.conf.json and start with ng serve --proxy-config proxy.conf.json.

If the API must live on another domain, allow only your app's exact origin with credentials, and add CSRF protection to state-changing requests. Angular's HttpClient has built-in support for the double-submit pattern: it reads an XSRF-TOKEN cookie and sends it back in an X-XSRF-TOKEN header, which your server then checks.

7. Testing and hardening

  • API tests: Jest or Vitest with Supertest, covering wrong passwords, expired and reused reset links, and a stale tokenVersion.
  • Angular tests: unit-test the service and guard with HttpTestingController, and run end-to-end sign-in flows with Playwright.
  • Security: add helmet for security headers, serve everything over HTTPS, and scan the running app with OWASP ZAP.
  • Logging: log failed logins, never passwords or tokens.

For refresh tokens and revocation, see JWT security best practices.

8. Running it on Domain India

WhereNode.js APIMongoDB
cPanel or DirectAdmin shared hostingSetup Node.js App in the control panelNot available: MongoDB can't be reached from shared hosting, so use MySQL or PostgreSQL instead
App PlatformNode.js apps are detected and built automaticallyA hosted MongoDB service; test the connection from your app
VPSYour own Node.js, Nginx and PM2, self-managedInstall it on the VPS or use a hosted service

For a full MEAN deployment, a VPS or the App Platform fits best. Running MERN or MEAN on a clean VPS covers Nginx, PM2 and MongoDB, and getting started with the App Platform covers deploying there. Test that your password-reset emails are delivered before you rely on them.

App Starter
₹100/mo + GST
  • 512 MB RAM per app
  • 1 vCPU
  • 5 GB NVMe SSD
  • PostgreSQL Database
See plan details
VPS Starter
₹552.65/mo + GST
  • 1 vCPU
  • 2 GB DDR4 RAM
  • 64 GB NVMe SSD Storage
  • 2 TB Monthly Bandwidth
See plan details

Prices on the cards are live and exclude 18% GST.

Where should an Angular app store the login token?

Nowhere. Let the server set it as an httpOnly, Secure, SameSite cookie, and have Angular send requests with credentials. JavaScript can't read an httpOnly cookie, so a cross-site scripting bug can't steal the session, whereas anything in localStorage can be read by any script on the page.

How do I send cookies with Angular HttpClient?

Set withCredentials to true on the request. The simplest way is a functional HTTP interceptor registered with provideHttpClient and withInterceptors that clones every request with withCredentials: true.

Is an Angular route guard enough to protect data?

No. A guard only decides which screens the user sees. Every API route that returns private data must check the session on the server, because anyone can call the API directly.

Which password hashing should a MEAN app use?

bcrypt with a cost of 12, or Argon2id. Never store plain passwords or use a fast hash such as SHA-256 or MD5 for passwords.

How should password reset tokens be handled?

Generate at least 32 random bytes, email the token as a link, store only its SHA-256 hash with a short expiry such as 30 minutes, clear it after use, and sign the user out of other sessions after the reset.

Can I run a MEAN app on Domain India shared hosting?

The Node.js part can run with Setup Node.js App, but MongoDB can't be reached from shared hosting. Use the App Platform with a hosted MongoDB service, a VPS, or switch the database to MySQL or PostgreSQL.

Ready to deploy? Compare the App Platform and VPS plans, or open a support ticket if you're not sure which fits your app.

Deploy your MEAN app

Node.js apps are detected and built automatically, with free SSL and custom domains.

See App Platform plans

Was this article helpful?

Your answer helps us decide what to improve next.

Still need help? Open a support ticket and our team will reply.

Prefer an app? Add this site to your home screen.Get the app
MEAN Stack User Authentication Service (2026 Guide)