Expose Meteor user context to Express endpoints

Hello community!

I recently pushed a PR that allows Express endpoints defined through the Meteor WebApp package to apply authorization. This makes it possible to protect specific REST endpoints in a way similar to how DDP endpoints like methods and subscriptions already work.

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

import { WebApp } from 'meteor/webapp';
import { Accounts } from 'meteor/accounts-base';

// Apply auth middleware to /api routes
WebApp.connectHandlers.use('/api', Accounts.auth());

// Protected endpoint under /api
WebApp.connectHandlers.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 });
  }
});

On the client side Meteor.fetchWithAuth is available to inject the current user token to the endpoint call:

async function getUsers() {
  const response = await Meteor.fetchWithAuth('/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"
}

By default, Accounts.auth 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 Accounts.auth({ required: true }), which will reject unauthenticated requests automatically.


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.

12 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.

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