Expose Meteor user context to Express endpoints

Hello community!

I recently pushed a PR that introduces a dedicated package, accounts-express, for authenticating Express endpoints defined through Meteor’s WebApp package. It makes it possible to protect specific REST endpoints in a way similar to how DDP endpoints like methods and subscriptions already work, and it also ships an auth-aware fetch so the client (and the server) can call those endpoints with the user’s token attached.

The final shape splits cleanly into two pieces: an auth middleware for protecting endpoints, and an authenticated fetch for calling them.

Auth middleware

createAuthMiddleware reads a Meteor login token from the Authorization: Bearer header (or the meteor_login_token cookie when HttpOnly cookies are enabled) and populates req.userId plus Meteor.userId() for the rest of the chain.

As an example, on the server you can define the following:

import { WebApp } from 'meteor/webapp';
import { createAuthMiddleware } from 'meteor/accounts-express';

// Apply auth middleware to /api routes
WebApp.handlers.use('/api', createAuthMiddleware());

// Protected endpoint under /api
WebApp.handlers.use('/api/users', async (req, res) => {
  try {
    // User context is available
    console.log('Authenticated user ID:', Meteor.userId(), req.userId);
    console.log('Authenticated user:', await Meteor.userAsync());

    if (!Meteor.userId()) {
      res.status(401).json({ error: 'Unauthorized' });
      return;
    }

    const users = await Meteor.users
      .find({}, { fields: { username: 1, createdAt: 1, updatedAt: 1 } })
      .fetchAsync();

    res.json(users);
  } catch (error) {
    console.log(error);
    res.status(500).json({ error: error.message });
  }
});

By default, createAuthMiddleware does not require authentication, but it can detect the logged-in user. This is closer to how methods and subscriptions handle authorization logic, as it lets you customize authorization once you have access to the userId that attempted to reach the endpoint.

If you want to protect endpoints unconditionally, you can apply the middleware with createAuthMiddleware({ required: true }), which will reject unauthenticated requests automatically:

WebApp.handlers.use('/api/admin', createAuthMiddleware({ required: true }));

Authenticated fetch

On the client side, three fetch entry points become available so you can choose how aggressively the login token is attached:

Entry point Default When to reach for it
Meteor.fetch auth: false (opt-in) Neutral fetch that becomes auth-aware when you pass auth: true.
import { fetch } from 'meteor/fetch' auth: false (opt-in) Generic fetch shim. Stays neutral so installing accounts-express doesn’t silently start attaching the user’s token to arbitrary URLs.
import { fetch } from 'meteor/accounts-express' auth: true (opt-out) Auth-on-by-default. Pass auth: false to opt out for a single call.

The asymmetry is deliberate: importing from meteor/accounts-express is the opt-in. The other two entry points are shared with non-auth code paths and stay neutral until you ask for auth.

So the protected call from the example becomes:

import { fetch } from 'meteor/accounts-express';

async function getUsers() {
  const response = await fetch('/api/users');
  return response.json();
}

console.log(await getUsers());

When logged in and authorized the console logs:

[
  {
    "_id": "Ystii57EhB6aFyXup",
    "createdAt": "2026-01-20T16:11:09.406Z",
    "username": "meteorite"
  }
]

When logged out, given the error handled within the endpoint:

{
  "error": "Unauthorized"
}

If you want full control over which calls carry the token, prefer the opt-in form:

import { fetch } from 'meteor/fetch';

const protectedRes = await Meteor.fetch('/api/users', { auth: true });
const alsoProtected = await fetch('/api/users', { auth: true });
const publicRes    = await Meteor.fetch('/api/public');

On the server you can also pass an explicit { token } to call an endpoint as a specific user, instead of relying on the current request context.

:page_facing_up:Documentation:

Coming next: REST login/logout endpoints

The next iteration introduces drop-in factories createLoginMiddleware and createLogoutMiddleware that turn the password login flow into proper REST endpoints, issuing the same login tokens DDP login produces, and firing the standard validateLoginAttempt / onLogin / onLoginFailure / onLogout hooks.


I opened this forum discussion because, even though the implementation is done, covered by tests, and already used for a long time in my own projects, I would like feedback on the developer experience when moving it to the core. The goal is to discuss details early and make decisions together.

This is an experimental feature planned for a future minor release, likely Meteor 3.5+ (with a beta launched as soon as possible). I think it is worth bringing up now and discussing it ahead of time, so the deliver can coverage all planned usage scenarios.

16 Likes

Oooh, very interesting! I guess the benefit of using the same context is now you can use the same underlying function for a WebApp endpoint or a Meteor methods endpoint…

I just had a quick skim through the code and I can see you’re handling the expiring tokens which was the one thing I wanted to check :laughing:

As later additions?, I think it’d be nice to also have:

  • Login (with login token generation, expiries etc all consistent with DDP login)
  • Logout
  • Arbitrary tokens e.g. a long-lived API Token.

The first two I think were covered by this user’s custom middleware:

I think after all that, Meteor’s half way to internalising the same kind of functionality simple-rest etc had into the core (the next step would be figuring out how to safety expose HTTP endpoints for pub/sub, methods etc :sweat_smile:). Still perhaps best left to a 3rd party package (or alternatively, the separate efforts to improve Meteor method declarations), but at least the accounts plumbing is a lot simpler!

3 Likes

Your suggestions are really interesting, thank you. Also, I like how you linked previous discussions on this matter, and the existence of the legacy meteor-rest package on Meteor 2.x. All this can influence future decisions around the Meteor-Express integration for Meteor 3.x.

As mentioned above, and since there is still some time before delivering the feature in a minor Meteor release (3.5+), I will iterate on your suggestions and, at minimum, include the changes that are closely related to Express user context management (login/retrieve token, logout/clean tokens, arbitrary tokens). The rest can come in later iterations, if it proves to have other implications, touches other areas or result harder to solve them.

1 Like

This looks great! I also have a scenario to discuss with everyone. My application is based on SSR (Server-Side Rendering). To accelerate page access for logged-in users, I need to handle pages under a logged-in state. Currently, I achieve this by setting a login_token cookie after login. On the server side, I retrieve the login_token cookie and query the corresponding user to render the post-login page.

On the client side, I use:

function resetToken() {
  const loginToken = Meteor._localStorage.getItem('Meteor.loginToken');
  const loginTokenExpires = new Date(Meteor._localStorage.getItem('Meteor.loginTokenExpires')!);

  if (loginToken) {
    setToken(loginToken, loginTokenExpires);
  } else {
    setToken(null, -1);
  }
}

function setToken(loginToken: string | null, expires: Date | number) {
  let cookieString = `meteor_login_token=${encodeURIComponent(loginToken ?? '')}`;
  let date;

  if (typeof expires === 'number') {
    date = new Date();
    date.setDate(date.getDate() + expires);
  } else {
    date = expires;
  }
  cookieString += `; expires=${date.toUTCString()}; path=/`;

  // https://developer.mozilla.org/docs/Web/HTTP/Headers/Set-Cookie/SameSite
  cookieString += `; SameSite=Lax`;

  document.cookie = cookieString;
}

On the server side:

const loginToken = sink.request.cookies['meteor_login_token'];

const getUserByLoginToken = async (loginToken?: string) => {
  if (!loginToken) return null;

  const hashedToken = Accounts._hashLoginToken(loginToken);

  const user = await UserCollection.findOneAsync(
    { 'services.resume.loginTokens.hashedToken': hashedToken },
    { fields: securFields }
  );

  return user;
};

Additionally, I use Redux Toolkit and inject the user into the store, accessing the user object throughout the code with:

const user = useSelector(state => state.user);

I understand that Meteor’s design does not natively use cookies, but when using SSR to accelerate access for logged-in users, cookies seem to be the only viable option. Could Meteor consider offering a cookie-based solution in the future to support SSR-accelerated access scenarios?

As a side note: In my application, using SSR improved the page load time from around 3–4 seconds down to about 1 second.

in terms of DX, why not rename fetchWithAuth to request, we could add more resources to this functin in te future, like sanitization, types check in the client side, IDK, just brainstorming

the usage would be Meteor.request(url, {auth: false}) where the token is setup in the header as default

Published beta version: 3.5-beta.10

Good news for those who want to start using authentication and authorization on Express endpoints in Meteor apps, using the Meteor accounts packages and keeping the same session approach as any Meteor app.

Try it with:

meteor update --release 3.5-beta.10

There is a demo app to explore features and examples:

https://accounts-express.sandbox.galaxycloud.app

Log in as a demo user and press the call buttons. You will see the client and server code involved, and the response from the Express endpoint with authentication. You can also check it in your browser’s Network tab.

We need community testing on real apps to verify everything works as expected. So far, things are working well, and we will continue with these checks.

:page_facing_up: Documentation

I updated the first post of this forum thread to reflect the latest version of the delivered changes for accounts-express, where you can learn more about it and see a code example.

For more documentation, refer to:

What’s Next

As @ceigey proposed, login and logout endpoints, along with better handling of long-lived API tokens, will be implemented next. These updates are planned for Meteor 3.6 or later minor versions.

A PR with login/logout endpoints is already started and working. More testing is still needed and will come later.

5 Likes

Nice, looking forward to seeing more of this! Thank you for your work on this Nacho (and sorry for the radio silence with your earlier response).

This will potentially streamline some automation scenarios we were thinking of at the company I work at, that weren’t quite high priority enough to justify building out an entire authenticated API layer on top of.

2 Likes