Skip to content

Auth Wiring

Recipes are working wiring examples with the sharp edges annotated. This page and the auth-wiring recipe served to coding agents by the MCP server share one source (@modularityjs/manifest), so they never drift apart.

The auth system has two pools you must not confuse:

  • AuthResolversPooltoken → AuthIdentity. Populated by driver modules (AuthJwtModule.forRoot({...}) auto-registers JwtAuthService here). Used by AuthService.resolve(token).
  • HttpAuthenticatorsPoolHttpRequest → AuthIdentity. Populated by HTTP-bridge packages. HttpAuthModule (the bridge) ships no defaults: it iterates the pool on onRequest and attaches the first match to request.auth. If you load only HttpAuthModule and contribute nothing, request.auth is always undefined.

For Bearer JWT, the ready-made bridge HttpAuthJwtModule reads Authorization: Bearer <token> and delegates to JwtAuthService. For anything else (custom header, query token, app-specific scheme), contribute an HttpAuthenticator yourself.

Minimum wiring (JWT-protected /me, custom authenticator shown for clarity — HttpAuthJwtModule does the same out of the box):

typescript
import { AuthIdentity, AuthModule, AuthService } from '@modularityjs/auth';
import { AuthJwtModule, JwtAuthService } from '@modularityjs/auth-jwt';
import { Inject, Injectable } from '@modularityjs/di';
import {
  Body,
  Controller,
  Get,
  HttpControllersPool,
  HttpModule,
  HttpRequest,
  Post,
} from '@modularityjs/http';
import {
  Auth,
  HttpAuthenticator,
  HttpAuthenticatorsPool,
  HttpAuthModule,
  RequireAuth,
} from '@modularityjs/http-auth';
import { HttpFastifyModule } from '@modularityjs/http-fastify';
import { Module } from '@modularityjs/modularity';

@Injectable()
class BearerAuthenticator implements HttpAuthenticator {
  constructor(@Inject(AuthService) private readonly auth: AuthService) {}

  async authenticate(request: HttpRequest): Promise<AuthIdentity | undefined> {
    const header = request.headers['authorization'];
    if (typeof header !== 'string' || !header.startsWith('Bearer '))
      return undefined;
    return this.auth.resolve(header.slice(7));
  }
}

@Controller('/auth')
class LoginController {
  constructor(@Inject(JwtAuthService) private readonly jwt: JwtAuthService) {}

  @Post('/login')
  async login(@Body() body: { name: string }) {
    const token = await this.jwt.sign({
      id: crypto.randomUUID(),
      attributes: { name: body.name },
    });
    return { token };
  }
}

@Controller('/me')
class MeController {
  @Get()
  @RequireAuth() // 401 before the handler runs if request.auth is missing
  me(@Auth() identity: AuthIdentity) {
    return identity;
  }
}

@Module({
  name: 'app',
  imports: [HttpModule, AuthModule, HttpAuthModule],
  providers: [BearerAuthenticator, LoginController, MeController],
  pools: [
    { pool: HttpControllersPool, key: 'login', useClass: LoginController },
    { pool: HttpControllersPool, key: 'me', useClass: MeController },
    {
      pool: HttpAuthenticatorsPool,
      key: 'bearer',
      useClass: BearerAuthenticator,
    },
  ],
})
class AppModule {}

// modules: [
//   ModularityModule,
//   HttpModule.forRoot({ port: 3000 }),
//   HttpFastifyModule,
//   AuthModule,
//   AuthJwtModule.forRoot({ secret: process.env.JWT_SECRET!, issuer: 'my-app', audience: 'my-app' }),
//   HttpAuthModule,
//   AppModule,
// ]

@Auth() by itself returns undefined when authentication is absent or invalid — it does not auto-reject. Pair it with @RequireAuth() (class- or method-level, from @modularityjs/http-auth) so the adapter returns 401 before your handler runs. For routes that also need a specific permission, use @RequirePermission() from @modularityjs/http-authz — it implies @RequireAuth() (401 unauthenticated, 403 unauthorized).