Skip to content

Auth Middleware ​

The auth middleware validates sessions, builds CASL abilities, and optionally enforces authorization on routes.

Configuration ​

Create auth middleware using createAuthMiddleware():

typescript
import { createAuthMiddleware, createBetterAuthProvider } from 'glasswork/auth';
import { auth } from './auth.config';
import { abilities } from './abilities';

const provider = createBetterAuthProvider({
  auth,
  mapUser: (user) => ({
    id: user.id as string,
    email: user.email as string,
    role: (user.role as string) ?? 'member',
    tenantId: user.organizationId as string | undefined,
  }),
});

export const authMiddleware = createAuthMiddleware({
  provider,
  buildAbility: (user) => abilities.for(user),
});

Options ​

OptionTypeDefaultDescription
providerAuthProviderRequiredAuth provider (e.g., Better Auth wrapper)
buildAbility(user) => AbilityRequiredFunction to build CASL ability from user
cookieNamestringProvider setting or 'session'Cookie name for session token. The Better Auth provider reads its resolved cookie configuration, including secure prefixes and custom names.
headerNamestring'Authorization'Header name for Bearer token
allowGuestbooleantrueAllow unauthenticated requests
guestAbility() => AbilityEmpty abilityAbility for guest users
onInvalidSession(c) => void-Callback when session is invalid

Usage ​

Apply to All Routes ​

Register middleware before the handlers it protects:

typescript
import { authMiddleware } from './auth/auth.middleware';

// Add to all routes in a router
router.use('*', authMiddleware());

// Or to all API routes
app.use('/api/*', authMiddleware());

Do not add auth middleware to the application after bootstrap(): module routes have already been registered at that point, so later middleware does not wrap them. Apply it inside a module's route factory before registering protected handlers.

Route-Level Authorization ​

Pass an authorize object to require specific permissions:

typescript
router.get('/projects',
  authMiddleware({ action: 'read', subject: 'Project' }),
  handler
);

Or use the authorize option in the route() helper:

typescript
router.get('/projects', ...route({
  authorize: { action: 'read', subject: 'Project' },
  responses: { 200: ProjectListSchema },
  handler: async ({ ability }) => {
    return projectService.findAll(ability);
  },
}));

Tenant Context with RLS ​

For PostgreSQL Row Level Security, apply tenant middleware after session validation and before registering protected routes:

typescript
import { createRLSMiddleware } from 'glasswork/rls';
import { authMiddleware } from './auth/auth.middleware';

router.use('*', authMiddleware());
router.use('*', createRLSMiddleware({ allowUnauthenticated: false }));

The default extractor reads tenantId from the authenticated user. Populate it from a validated session or verified membership, as in the provider's mapUser above. RLS middleware binds context; CASL still determines permitted actions.

If validating the session itself needs access before a tenant is known, scope an explicit bypass to that provider operation. Never wrap the downstream request in the bypass. See HTTP and Authentication for the required extension and policy configuration.

Session Resolution ​

The middleware extracts session tokens in this order:

  1. Cookie - Looks for cookieName (default: session)
  2. Bearer Token - Parses Authorization: Bearer <token> header

If a token is found, it's validated via the provider. Invalid sessions automatically clear the cookie after the response.

Guest Access ​

By default, unauthenticated requests are allowed with an empty ability. Configure guest behavior:

typescript
export const authMiddleware = createAuthMiddleware({
  provider,
  buildAbility: (user) => abilities.for(user),

  // Allow guests (default: true)
  allowGuest: true,

  // Define what guests can do
  guestAbility: () => abilities.forRole('guest'),
});

Requiring Authentication ​

To require authentication, either:

Option 1: Set allowGuest: false globally:

typescript
export const authMiddleware = createAuthMiddleware({
  provider,
  buildAbility: (user) => abilities.for(user),
  allowGuest: false, // 401 for unauthenticated requests
});

Option 2: Use authorize on specific routes - unauthenticated users get 401:

typescript
router.get('/profile', ...route({
  authorize: { action: 'read', subject: 'User' },
  // 401 if not authenticated, 403 if authenticated but no permission
}));

Error Handling ​

ScenarioResponse
No token, allowGuest: trueContinues with user: null
No token, allowGuest: false401 Unauthorized
Invalid token, allowGuest: trueCookie cleared, continues with user: null
Invalid token, allowGuest: falseCookie cleared, returns 401 Unauthorized
Authorization fails, not authenticated401 Unauthorized
Authorization fails, authenticated403 Forbidden

Custom Error Messages ​

Errors use Glasswork's standard exceptions:

typescript
// From middleware - can be caught in error handler
import { UnauthorizedException, ForbiddenException } from 'glasswork/http';

// In handler - for custom checks
handler: async ({ ability }) => {
  if (!ability.can('delete', 'Project')) {
    throw new ForbiddenException('Only admins can delete projects');
  }
}

Context Values ​

The middleware sets these values on the Hono context:

typescript
// Available in handlers
handler: async ({ user, session, ability, isAuthenticated, c }) => {
  // TypeScript-aware access
  console.log(user?.id);        // AuthUser | null
  console.log(session?.id);     // AuthSession | null
  console.log(isAuthenticated); // boolean

  // Use ability for checks
  if (ability.can('manage', 'all')) {
    // Admin access
  }

  // Direct context access (same values)
  c.get('user');
  c.get('ability');
}

Type Definitions ​

typescript
interface AuthUser {
  id: string;
  email?: string;
  role: string;
  tenantId?: string;
  [key: string]: unknown;
}

interface AuthSession {
  id: string;
  userId: string;
  expiresAt: Date;
  createdAt: Date;
  lastAccessedAt?: Date;
  metadata?: Record<string, unknown>;
}

Invalid Session Handling ​

When a session token is invalid or expired:

  1. The session cookie is deleted
  2. Optional onInvalidSession callback is invoked
  3. With allowGuest: true, the response proceeds with user: null; with allowGuest: false, the middleware returns 401 Unauthorized
typescript
export const authMiddleware = createAuthMiddleware({
  provider,
  buildAbility: (user) => abilities.for(user),
  onInvalidSession: async (c) => {
    // Log, track metrics, etc.
    console.log('Invalid session cleared');
  },
});

Custom Auth Providers ​

For auth systems other than Better Auth, implement the AuthProvider interface:

typescript
import { type AuthProvider, type AuthUser, type AuthSession } from 'glasswork/auth';

const customProvider: AuthProvider = {
  name: 'custom',

  async validateSession(token: string) {
    // Validate token with your auth system
    const session = await myAuthSystem.validate(token);

    if (!session) return null;

    return {
      session: {
        id: session.id,
        userId: session.userId,
        expiresAt: session.expiresAt,
        createdAt: session.createdAt,
      },
      user: {
        id: session.user.id,
        email: session.user.email,
        role: session.user.role,
      },
    };
  },

  async invalidateSession(token: string) {
    await myAuthSystem.revoke(token);
  },

  async refreshSession(token: string) {
    return myAuthSystem.refresh(token);
  },
};

Next Steps ​

Released under the MIT License.