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.