Plug-and-play JWT authentication infrastructure for NestJS microservices. One module registration wires up guards, strategies, and a runtime guard orchestrator — so your services share a consistent auth layer without repeating boilerplate.
- Overview
- Features
- Installation
- Quick Start
- Module Configuration
- Guards
- Decorators
- Custom Payload Validation
- Custom Guards
- Interfaces & Types
- Exceptions
- Token Reference
- Contributing
- License
@may-salguedo/auth-common solves a common problem in NestJS microservice architectures: every service needs JWT auth, but implementing guards, strategies, and per-route overrides from scratch in each service leads to drift and duplicated code.
This library provides a single AuthCommonModule.forRoot() call that:
- Registers a
JwtStrategybacked bypassport-jwt. - Provides a
JwtGuardextending NestJS'sAuthGuard('jwt'). - Wires an
OrchestratorGuard— a smart global guard that reads route metadata and delegates to the correct named guard at runtime. - Exposes clean decorators (
@PublicGuard(),@UseGuards()) for per-route control.
- 🔐 JWT out of the box — strategy + guard configured from a single secret.
- 🎯 Guard orchestration — one global guard, multiple named strategies resolved per route via metadata.
- 🌐 Public route bypass — mark any endpoint with
@PublicGuard()to skip auth entirely. - 🔌 Extensible — register any number of custom guards (
api-key,roles,subscription) alongside JWT. - 🧩 Custom payload validation — inject your own
validate()function to enrich or reject the decoded token payload. - 📦 Minimal peer dependencies — only requires the standard NestJS core packages.
pnpm add @may-salguedo/auth-commonMake sure the following packages are present in your project:
pnpm add @nestjs/common @nestjs/core reflect-metadata rxjsImport AuthCommonModule in your root AppModule using forRoot():
// app.module.ts
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { AuthCommonModule, OrchestratorGuard } from '@may-salguedo/auth-common';
@Module({
imports: [
AuthCommonModule.forRoot({
jwtSecret: process.env.JWT_SECRET,
}),
],
providers: [
{
provide: APP_GUARD,
useClass: OrchestratorGuard,
},
],
})
export class AppModule {}That's it. Every route is now JWT-protected by default. Use the decorators below to opt individual routes in or out.
Returns a DynamicModule that configures and exports the entire auth infrastructure.
AuthCommonModule.forRoot<T>(options: AuthCommonOptions<T>): DynamicModule| Parameter | Type | Required | Description |
|---|---|---|---|
options |
AuthCommonOptions<T> |
✅ | Configuration object — see table below. |
export interface AuthCommonOptions<T = any> {
jwtSecret: string;
defaultGuard?: string;
guards?: Record<string, Type<CanActivate> | CanActivate>;
validate?: (payload: T & TokenIssues) => (T & TokenIssues) | Promise<T & TokenIssues>;
}| Property | Type | Default | Description |
|---|---|---|---|
jwtSecret |
string |
— | Required. Secret used to verify incoming JWT tokens. |
defaultGuard |
string |
'jwt' |
Key of the guard the OrchestratorGuard falls back to when no @UseGuards() metadata is present on a route. |
guards |
Record<string, Type<CanActivate> | CanActivate> |
{} |
Named custom guards to register alongside JWT. See Custom Guards. |
validate |
(payload: T & TokenIssues) => T & TokenIssues |
identity |
Optional function to enrich or validate the decoded token payload. See Custom Payload Validation. |
The central piece of the library. Designed to be registered as a global guard via APP_GUARD, it intercepts every request and decides which guard(s) to execute based on route metadata:
| Condition | Behaviour |
|---|---|
Route is decorated with @PublicGuard() |
Passes immediately — no guard is executed. |
Route is decorated with @UseGuards('key1', 'key2') |
Executes the named guards sequentially. |
| No decorator is present | Falls back to the defaultGuard configured in forRoot(). |
// main.ts — alternative: apply globally via useGlobalGuards
const app = await NestFactory.create(AppModule);
// If not using APP_GUARD provider, apply manually:
// app.useGlobalGuards(app.get(OrchestratorGuard));Note: Using
APP_GUARDis preferred because it participates in NestJS's dependency injection, allowing the guard to have its own injected services.
A standard Passport-backed JWT guard. It is automatically registered by AuthCommonModule and is available under the 'jwt' key in the guard registry.
You can also inject and use it directly:
import { UseGuards } from '@nestjs/common';
import { JwtGuard } from '@may-salguedo/auth-common';
@Controller('protected')
@UseGuards(JwtGuard)
export class ProtectedController {}Marks a route or an entire controller as publicly accessible, bypassing the OrchestratorGuard entirely.
import { Controller, Get } from '@nestjs/common';
import { PublicGuard } from '@may-salguedo/auth-common';
@Controller('auth')
export class AuthController {
@PublicGuard()
@Get('login')
login() {
}
@Get('me')
getProfile() {
}
}Can also be applied at controller level to make all routes public:
@PublicGuard()
@Controller('public')
export class PublicController {}
⚠️ This is not NestJS's built-in@UseGuards(). Import it from@may-salguedo/auth-commonto work with the orchestrator.
Specifies which named guard(s) the OrchestratorGuard should execute for a given route. Guards are executed sequentially — all must pass for the request to proceed.
import { Controller, Get } from '@nestjs/common';
import { UseGuards } from '@may-salguedo/auth-common';
@Controller('admin')
export class AdminController {
@UseGuards('jwt', 'roles')
@Get('dashboard')
getDashboard() {
}
@UseGuards('api-key')
@Get('webhook')
handleWebhook() {
}
}Named keys must match the keys provided in the guards option of forRoot().
By default, JwtStrategy returns the decoded JWT payload as-is. Provide a validate function to enrich the payload (e.g. fetch the user from a database) or reject it by throwing an exception.
// app.module.ts
import { AuthCommonModule } from '@may-salguedo/auth-common';
import { UnauthorizedException } from '@nestjs/common';
interface MyPayload {
sub: string;
email: string;
}
AuthCommonModule.forRoot<MyPayload>({
jwtSecret: process.env.JWT_SECRET,
validate: async (payload) => {
const user = await userRepository.findOne(payload.sub);
if (!user || !user.isActive) {
throw new UnauthorizedException('User not found or inactive');
}
return { ...payload, roles: user.roles };
},
});The return value of validate() becomes the value attached to request.user in your controllers.
Register any additional guard under a named key to make it available to @UseGuards() and OrchestratorGuard:
// api-key.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
@Injectable()
export class ApiKeyGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
return request.headers['x-api-key'] === process.env.API_KEY;
}
}// app.module.ts
AuthCommonModule.forRoot({
jwtSecret: process.env.JWT_SECRET,
defaultGuard: 'jwt',
guards: {
'api-key': ApiKeyGuard,
'custom': new SomeGuardInstance(),
},
});// any controller
@UseGuards('api-key')
@Post('ingest')
handleIngestion() {}Configuration interface for forRoot(). See Module Configuration.
Standard JWT claims automatically merged into every decoded payload:
export interface TokenIssues {
iat: number;
exp: number;
}Your custom payload type is always intersected with TokenIssues when received by validate().
An HttpException that produces a 424 Failed Dependency response. Useful inside validate() when the auth failure is caused by a downstream dependency (e.g. user service unavailable) rather than an invalid token.
import { FailedDependencyException } from '@may-salguedo/auth-common';
validate: async (payload) => {
const user = await userService.find(payload.sub).catch(() => {
throw new FailedDependencyException('User service unreachable');
});
return { ...payload, ...user };
}| Constructor | Default message |
|---|---|
new FailedDependencyException() |
'Failed Dependency' |
new FailedDependencyException('Custom message') |
'Custom message' |
The following DI injection tokens are exported for advanced scenarios where you need to inject library internals into your own providers:
| Token | Type | Description |
|---|---|---|
JWT_SECRET |
string |
The secret passed to forRoot(). |
DEFAULT_GUARD |
string |
The defaultGuard key passed to forRoot(). |
GUARD_REGISTRY |
Record<string, CanActivate> |
Map of all registered named guards, including 'jwt'. |
IS_PUBLIC_KEY |
string |
Metadata key used by @PublicGuard(). |
USE_GUARDS_KEY |
string |
Metadata key used by @UseGuards(). |
import { Inject } from '@nestjs/common';
import { GUARD_REGISTRY } from '@may-salguedo/auth-common';
@Injectable()
export class MyService {
constructor(
@Inject(GUARD_REGISTRY) private registry: Record<string, CanActivate>
) {}
}| Command | Description |
|---|---|
pnpm test |
Run unit tests once. |
pnpm test:watch |
Run tests in watch mode (auto-rerun on changes). |
pnpm test:cov |
Run tests with coverage report (fails if coverage < 90%). |
pnpm build |
Compile TypeScript to dist/ using NestJS builder. |
pnpm pack |
Create a .tgz archive of the package (dry-run for publishing). |
pnpm output |
Print absolute path of the generated .tgz file (used after pack). |
pnpm compile |
Full local build pipeline: build pack output. |
pnpm lint |
Run ESLint and auto-fix issues. |
pnpm lint:no-spec |
Run ESLint excluding spec files. |
pnpm format |
Format code with Prettier. |
pnpm check |
TypeScript type-check without emitting files. |
pnpm audit |
Check for vulnerabilities (fails on any severity). |
pnpm audit:trivy |
Run Trivy filesystem scan (vulns, misconfigs, licenses). |
The repository includes a multi-stage Dockerfile under .github/docker/ and a docker-compose.yml at the root to run every job inside a consistent Node.js environment.
docker compose build test
docker compose run --rm test
docker compose run --rm test pnpm test -- src/common/tokens.spec.tsThe same compose file supports all CI jobs:
docker compose build audit
docker compose run --rm audit
docker compose build linter
docker compose run --rm linter
docker compose build check
docker compose run --rm check
docker compose build build
docker compose run --rm buildCoverage reports are written at ./coverage/lcov-report/index.html when using the provided compose setup.
You can simulate the CI/CD pipeline on your local machine using act. The repository includes event files under .github/events/:
| Event file | Triggers | Workflow |
|---|---|---|
push-develop.json |
Push to develop |
CI |
push-main.json |
Push to main |
CI |
push-tag.json |
Push tag v* |
CD |
pull-request-develop.json |
PR against develop |
CI |
pull-request-main.json |
PR against main |
CI |
workflow-dispatch.json |
Manual dispatch | CI / CD |
Install act and list available jobs:
act -l# Push to develop
act -W .github/workflows/ci.yml -e .github/events/push-develop.json
# PR against develop
act -W .github/workflows/ci.yml -e .github/events/pull-request-develop.jsonact -W .github/workflows/cd.yml -e .github/events/push-tag.json \
-j trivy-scan --artifact-server-path /tmp/act-artifacts \
--secret-file .secretsThe --artifact-server-path flag is required for the artifact upload/download actions to work locally. Using -j trivy-scan runs the full dependency chain (wait-for-ci → setup → trivy-scan).
The
wait-for-cijob is automatically skipped when running inact(it detects theACT=trueenvironment variable) since there is no real CI workflow run to check. On GitHub, it polls the CI workflow for the same commit and blocks CD until CI succeeds.
To test the publish and release jobs locally, add the required tokens to .secrets:
GITHUB_TOKEN=ghp_your_github_token_here
NPM_TOKEN=npm_your_npm_token_hereThen run:
act -W .github/workflows/cd.yml -e .github/events/push-tag.json \
--artifact-server-path /tmp/act-artifacts \
--secret-file .secretsThe
GITHUB_TOKENrequirespublic_reposcope foractto clone action repos and populategithub.token. For the full CD pipeline locally, the PAT also needswrite:packages(GitHub Packages) —contents: writeis included inpublic_repofor public repos, andpermissionsset in each CD job apply only on GitHub.
actuses Docker containers internally, so ensure Docker is installed and running. The provided event files match your GitHub Actions workflows (ci.yml,cd.yml) exactly.
The compile script is the recommended one-shot build command:
pnpm compileCreates a .tgz archive and prints its absolute path -- perfect for CI or for local installation in another project:
pnpm install /absolute/path/to/may-salguedo-auth-common-#.#.#.tgzThis project is licensed under the Eclipse Public License 2.0 (EPL-2.0). You may obtain a copy of the License at
https://www.eclipse.org/legal/epl-2.0/
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
Made with ❤️ by MaySalguedo