Add basic auth

The dashboard has no built-in auth. Don't expose it on the open internet without one. Here's the minimum per framework.

Express + Passport

From examples/with-express-auth.

const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const { ensureLoggedIn } = require('connect-ensure-login');
const session = require('express-session');

passport.use(new LocalStrategy((username, password, cb) => {
  if (username === 'bull' && password === 'board') {
    return cb(null, { user: 'bull-board' });
  }
  return cb(null, false);
}));

passport.serializeUser((user, cb) => cb(null, user));
passport.deserializeUser((user, cb) => cb(null, user));

app.use(session({ secret: 'keyboard cat', resave: true, saveUninitialized: true }));
app.use(passport.initialize());
app.use(passport.session());

app.post('/ui/login', passport.authenticate('local', { failureRedirect: '/ui/login?invalid=true' }),
  (req, res) => res.redirect('/ui'));

app.use('/ui', ensureLoggedIn({ redirectTo: '/ui/login' }), serverAdapter.getRouter());

A logged-in session reaches /ui without a second login, which is all "auto-login" really means for a cookie-based app. If your API uses bearer tokens instead, see Auto-login from a token-based frontend.

Run it:

git clone https://github.com/felixmosh/bull-board
cd bull-board/examples/with-express-auth
npm install && npm start
# http://localhost:3000/ui (login: bull / board)

Fastify + @fastify/basic-auth

From examples/with-fastify-auth.

await app.register(require('@fastify/basic-auth'), {
  validate: (username, password, req, reply, done) => {
    if (username === 'bull' && password === 'board') return done();
    done(new Error('Unauthorized'));
  },
  authenticate: { realm: 'Bull-Board' },
});

app.after(() => {
  const serverAdapter = new FastifyAdapter();
  createBullBoard({ queues: [new BullMQAdapter(queue)], serverAdapter });
  serverAdapter.setBasePath('/ui');
  app.register(serverAdapter.registerPlugin(), { prefix: '/ui' });

  app.addHook('onRequest', (req, reply, next) => {
    app.basicAuth(req, reply, (err) => err ? reply.code(401).send({ error: err.name }) : next());
  });
});

The onRequest hook covers every route registered after it. Scope the auth plugin inside a child context if you want it to cover only the dashboard.

Hapi + strategy

From examples/with-hapi-auth.

await app.register(require('@hapi/basic'));
app.auth.strategy('simple', 'basic', {
  validate: async (_req, username, password) => ({
    isValid: username === 'bull' && password === 'board',
    credentials: { username },
  }),
});

const serverAdapter = new HapiAdapter();
createBullBoard({ queues: [new BullMQAdapter(queue)], serverAdapter });
serverAdapter.setBasePath('/ui');

await app.register(
  { plugin: serverAdapter.registerPlugin(), options: { auth: 'simple' } },
  { routes: { prefix: '/ui' } }
);

The plugin options pass straight to Hapi's route config, so the auth strategy applies to every bull-board route.

NestJS + guards

From examples/with-nestjs-fastify-auth.

NestJS on the Fastify platform with a standard @UseGuards() guard. The example uses passport-local plus @fastify/secure-session for session cookies.

@Controller()
export class AppController {
  @Post('login')
  @UseFilters(AuthExceptionFilter)
  @UseGuards(AuthGuard('local'))
  login(@Request() req: FastifyRequest, @Response() reply: FastifyReply) {
    req.session.set('sev-data', req.user);
    return reply.status(302).redirect('/queues');
  }
}

The dashboard is mounted by @bull-board/nestjs, and the module's own guard checks the session before the route resolves.

Auto-login from a token-based frontend

If your app uses a cookie session, you already have auto-login. The browser sends the session cookie on every request, including when someone opens /ui, so a logged-in admin lands on the dashboard through your existing middleware without logging in again. There's nothing else to do.

Bearer tokens are where it gets awkward. When a separate SPA authenticates by sending an Authorization: Bearer header, opening /ui in a new tab won't carry that header. It's just a normal browser navigation, so your token middleware turns it away. What the dashboard needs is a cookie the browser will send on its own.

So give it one. Add an admin-only endpoint that logs the user into a session and hands back the URL:

// Guarded by your normal bearer-token middleware, admin only.
app.get('/api/queue-monitor', requireAdmin, (req, res) => {
  req.login(req.user, (err) => {          // sets the session cookie
    if (err) return res.status(500).end();
    res.json({ url: `${req.protocol}://${req.get('host')}/ui` });
  });
});

The SPA calls it with its token and opens the URL it gets back:

const { url } = await api.get('/api/queue-monitor');
window.open(url, '_blank', 'noopener,noreferrer');

The new tab now has a session cookie, so the ensureLoggedIn gate from the Express example lets it through. It's the same session a normal login would create; you're just creating it on demand.

Don't put the credentials in the URL. A link like https://user:pass@host/ui is an easy one-click shortcut, but those credentials end up in the address bar, the browser history, and referrer headers. Use a cookie.

Combine with read-only mode

Auth keeps strangers out. Read-only mode keeps authenticated users from running destructive actions. Use both for public-facing status boards.