npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

passport-await

v0.1.0

Published

async/await for Passport without the four ways the usual wrapper breaks: an async verify that throws kills the process, a promise around authenticate() never settles on an OAuth redirect or pass(), and a rest-args verify adapter silently shifts passport-o

Readme

passport-await

The usual async wrapper around Passport breaks in four ways.

Passport's API is callbacks. Its most-reacted open issue, #536 "Add support promises flow", has 122 👍 and has been open since 2017. The thread, and StackOverflow, fill the gap with a few lines of wrapper. Measured against real passport 0.7.0, a real Express server and a real OAuth2 server:

1. async verify that throws        connection dropped; server process exit code 1
2. promise around authenticate()   OAuth2 login leg: 302 sent, promise still pending after 2s
3. strategy calls pass()           promise rejects with `undefined`
4. (...args) verify adapter        passport-oauth2 reads verify.length === 0 and
                                   leaves out the token response (`params`, with id_token)

Those are the first tests in this repository. They assert that the naive version is broken. If one starts failing, passport changed, and the matching part of this package may no longer be needed.

Why each one happens:

  1. Crash. Strategies call verify(..., done) and ignore the return value. An async verify that throws returns a rejected promise nobody handles. Since Node 15 that ends the process. An Express error handler doesn't help, because the error never reaches Express.
  2. Hang. passport.authenticate(name, callback) only calls callback for success, fail and error. For redirect it writes the response itself, and for pass it calls next(). So a promise that waits on the callback never settles on the first leg of every OAuth login.
  3. undefined rejection. This is the same gap from the other side. When next is wired to reject, pass() looks like a failure with no reason.
  4. Wrong arguments, no error. passport-oauth2 picks between (accessToken, refreshToken, params, profile, done) and (accessToken, refreshToken, profile, done) based on verify.length. So do strategies that extend it — passport-google-oauth20, passport-github2 and passport-facebook all do. A (...args) => {} wrapper has length 0, so a verify written for params gets profile in that slot instead.

Use

npm install passport-await
import passport from 'passport';
import { Strategy as LocalStrategy } from 'passport-local';
import { authenticate, logIn, verify, reject } from 'passport-await';

passport.use(new LocalStrategy(verify(async (username, password) => {
  const user = await db.users.find(username);          // throwing here is a 500, not a crash
  if (!user) return reject({ message: 'no such user' });
  return (await argon2.verify(user.hash, password)) ? user : null;
})));

app.post('/login', async (req, res) => {
  const outcome = await authenticate('local', req);
  switch (outcome.type) {
    case 'success':  await logIn(req, outcome.user); return res.redirect('/');
    case 'redirect': return res.redirect(outcome.status, outcome.url);
    case 'fail':     return res.status(outcome.failures[0]?.status ?? 401).json(outcome.failures);
    case 'pass':     return res.status(401).end();
  }
});

authenticate() resolves with what the strategy decided and writes nothing to the response, so it works the same in an Express 5 handler, a GraphQL resolver, a Fastify route or a NestJS guard. You send the redirect yourself.

| | | |---|---| | verify(fn, { arity? }) | Async verify for any strategy. Return a user to succeed, anything falsy to fail, reject(info) / accept(user, info) for details. A rejection becomes done(err). | | authenticate(names, req, { strategyOptions?, authenticator?, signal? }) | Promise<{ type: 'success' \| 'fail' \| 'redirect' \| 'pass', ... }>. Errors reject. | | logIn(req, user) / logOut(req) | Promise versions. Both regenerate the session, and a session store failure rejects. | | serializeUser(fn) / deserializeUser(fn) | Async versions. A rejection becomes an error response. |

The arity rule, and where it still bites

The wrapper's length is fn.length + 1, so passport-oauth2 dispatches exactly as it would for a hand-written callback. Function.length stops counting at the first default or rest parameter:

async (accessToken, refreshToken, params, profile = {}) => ...   // length 3, not 4

In that case, pass verify(fn, { arity: 4 }). There is a test for this case, because that's where a correct wrapper still goes wrong without an error.

Chains behave like passport's

authenticate(['jwt', 'api-key'], req) tries strategies in order. A fail moves on to the next one. The first success, redirect, pass or error ends the chain. If every strategy fails, the outcome lists each failure with its strategy name, challenge and status. If a strategy decides twice, the first decision stands and the second is ignored. That is a strategy bug, but it shouldn't change the result.

A strategy that never decides leaves the promise pending. Passport has the same behaviour: the request hangs. Pass signal: AbortSignal.timeout(ms) to turn that into an AuthenticationAborted rejection that names the strategy.

What it does not do

  • No flash messages, successRedirect, failureRedirect, assignProperty or authInfo transformation. Those are passport's middleware writing to the response or session. The point here is to hand you the decision, so you write those two lines yourself.
  • It does not replace passport.initialize() / passport.session(). logIn needs them mounted and says so if they aren't.
  • Serializer chains (done('pass')) aren't supported by the async serializeUser / deserializeUser. Register those with passport directly.
  • It uses authenticator._strategy(name), which is passport's internal lookup, not a public API. The peer range is passport >= 0.6, and only 0.7.0 is tested.
  • Strategies tested: passport-local and passport-oauth2, plus small strategies that make one specific decision each. passport-jwt, OpenID Connect and SAML strategies are not tested. The only arity dispatch found in the strategies read was in passport-oauth2.
  • The test authorization server issues tokens for any code. These tests cover the full redirect → authorize → token → userinfo round trip. They don't cover whether a forged code is refused, because that is the server's job.

It is about 270 lines, so copying it is a reasonable choice. If you do, the tests are what to copy first.

Tests

npm install
npm run infra:up    # a real OAuth2/OIDC server and Redis
npm test            # 27 tests
npm run infra:down
  • The OAuth2 tests run the whole browser round trip against navikt/mock-oauth2-server. Despite the name, it is a real authorization server that signs real tokens. The access token that comes out is checked against its userinfo endpoint.
  • The crash tests run the server in a child process, so the exit code can be measured without taking the test runner down with it.
  • The session tests use connect-redis. The store-failure test uses a Redis ACL user that cannot DEL: loading and saving the session work, and only the destroy inside regenerate is refused. So the failure can only come from logIn.

Each guard was checked by removing it and running the suite. Without the length fix, 4 tests fail. Without converting rejections to done(err), the process-survives test fails. Without settling on redirect, 5 OAuth tests time out. If fail ends the chain, 3 tests fail. Without the decide-once guard, 1 test fails. That last test was added after the first mutation run passed without it: the test for a strategy that decides twice settled synchronously before the second call, so it could not see the guard.

Built with Claude

Claude wrote most of this code. The design and the choice of problem are mine. The problem came from a scan for widely used packages whose users are asking, in the issue tracker, for something the maintainer hasn't answered. The four failures above were measured before any of the package was written.

Licence

MIT.