Skip to content

Authentication & Authorization

This page shows a complete auth flow using WebMQ hooks on both server and client, combined with a React context hook for clean component usage.

The pattern: login endpoint issues a JWT → client stores it → client identify hook attaches it → server identify hook verifies it → server listen/publish hooks enforce role-based rules.

Server

Login endpoint validates credentials and returns a signed JWT with role:

import express from 'express';
import jwt from 'jsonwebtoken';

const SECRET = 'your-secret-key';
const app = express();

app.post('/login', express.json(), (req, res) => {
  const { username, password } = req.body;
  // Replace with real credential check
  if (password !== `${username}${username}`) {
    return res.status(403).json({ error: 'Invalid credentials' });
  }

  const role = username === 'admin' ? 'admin' : 'user';
  const token = jwt.sign({ username, role }, SECRET, { expiresIn: '1h' });
  res.json({ token, role });
});

WebMQ server hooks verify every identify, then authorize listen/publish by role:

import WebMQServer from 'webmq-backend';

const server = new WebMQServer({
  rmqUrl: 'amqp://localhost',
  exchange: 'auth_app',
  port: 8080
});

// Verify JWT on identify, store user in context
server.addHook('identify', async (header, context) => {
  const { token } = header;
  if (!token) throw new Error('Token required');
  const decoded = jwt.verify(token, SECRET);
  context.user = decoded; // { username, role }
  return header;
});

// Block any action if not identified
server.addHook('pre', async (header, context) => {
  if (!context.user) throw new Error('Not authenticated');
  return header;
});

// Authorize listen by role
server.addHook('listen', async (header, context) => {
  const { role, username } = context.user;
  const { bindingKey } = header;
  if (role === 'admin') return header; // can listen anywhere
  // Regular users can only hear their own topic
  if (bindingKey !== `user.${username}`) {
    throw new Error(`Cannot listen to ${bindingKey}`);
  }
  return header;
});

// Authorize publish by role
server.addHook('publish', async (header, context) => {
  const { role, username } = context.user;
  const { routingKey } = header;
  if (role === 'admin') return header; // can publish anywhere
  if (routingKey !== `user.${username}`) {
    throw new Error(`Cannot publish to ${routingKey}`);
  }
  return header;
});

await server.start();

Client (React)

AuthProvider manages auth state and the WebMQ client lifecycle:

import { createContext, useContext, useState, useEffect } from 'react';
import WebMQClient from 'webmq-frontend';

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
  const [token, setToken] = useState(null);
  const [role, setRole] = useState(null);
  const [wmqClient, setWmqClient] = useState(null);

  // WebMQ client follows token: connect on login, disconnect on logout
  useEffect(() => {
    if (!token) {
      wmqClient?.disconnect();
      setWmqClient(null);
      return;
    }
    const wmq = new WebMQClient({
      url: 'ws://localhost:8080',
      sessionId: crypto.randomUUID(),
    });
    wmq.addHook('identify', (h) => {
      h.token = token;
      return h;
    });
    wmq.connect();
    setWmqClient(wmq);
    return () => wmq.disconnect();
  }, [token]);

  const login = async (username, password) => {
    const res = await fetch('/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ username, password }),
    });
    if (!res.ok) throw new Error('Login failed');
    const { token: t, role: r } = await res.json();
    setToken(t);
    setRole(r);
  };

  const logout = () => {
    setToken(null);
    setRole(null);
  };

  return (
    <AuthContext.Provider value={{ token, role, wmqClient, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error('useAuth must be used within AuthProvider');
  return ctx;
}

Role-based UI gates content by auth state:

function App() {
  const { token, role, login, logout } = useAuth();

  if (!token) return <LoginForm onLogin={login} />;

  return (
    <div>
      <header>
        Logged in as {role}
        <button onClick={logout}>Logout</button>
      </header>
      {role === 'admin' ? <AdminPanel /> : <UserDashboard />}
    </div>
  );
}

function LoginForm({ onLogin }) {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    try { await onLogin(username, password); }
    catch { alert('Login failed'); }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={username} onChange={(e) => setUsername(e.target.value)} placeholder="Username" />
      <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password" />
      <button>Log in</button>
    </form>
  );
}

Admin and user components use the client through useAuth:

function AdminPanel() {
  const { wmqClient } = useAuth();
  // wmqClient.listen('admin.events', ...)
  // wmqClient.publish('admin.commands', ...)
}

function UserDashboard() {
  const { wmqClient } = useAuth();
  // wmqClient.listen('user.alice', ...)
  // wmqClient.publish('user.alice', ...)
}

Key points

  • Token sent once during identify, not on every message
  • Server context persists per-connection — user info available to all subsequent hooks
  • pre hook acts as a gate for all actions after identify
  • listen/publish hooks enforce role-based authorization at the messaging layer
  • React context keeps auth state and WebMQ client in sync — no manual disconnect/reconnect