Auth Plugin
Security is not a feature; it is an architecture. The AuthPlugin provides a modular, strategy-based system for handling Authentication (identifying who the user is) and Authorization (deciding what they can do).
Concept: The Security Checkpoint
- Authentication is the ID Scanner. It checks your badge (Token) to verify you are "Employee #42".
- Authorization is the Access Control List. It checks if "Employee #42" is allowed to enter "The Server Room".
- Guards are the Security Officers. They stand at the door of every Controller and enforce the rules.
The Strategy Pattern
Karin adopts the Strategy Pattern (popularized by Passport.js). This decouples the "How" from the "Where".
- The Strategy encapsulates the logic to extract and verify credentials (e.g., verifying a JWT signature, checking an OAuth callback).
- The Guard attaches the strategy to a specific route.
This means you can switch from Bearer Token to Cookie Session auth by simply changing the Strategy, without rewriting your Controllers.
Implementing a JWT Strategy
Your strategy must extend PassportStrategy. It is a Service, so it can inject dependencies (like your UserService to verify the user exists in the DB).
import { PassportStrategy, JwtService } from "@project-karin/auth";
import { Service, Inject, UnauthorizedException, ExecutionContext } from "@project-karin/core";
@Service()
export class JwtStrategy extends PassportStrategy {
constructor(
@Inject(JwtService) private jwt: JwtService,
@Inject(UserService) private users: UserService,
) {
super("jwt"); // The name used in AuthGuard('jwt')
}
// 1. Extraction & Verification Phase
async authenticate(context: ExecutionContext) {
const req = context.switchToHttp().getRequest();
const token = this.extractToken(req);
if (!token) return null; // Pass to next strategy or fail
try {
const payload = await this.jwt.verify(token);
return this.validate(payload);
} catch (e) {
throw new UnauthorizedException("Invalid Token");
}
}
// 2. Validation Phase (Business Logic)
async validate(payload: any) {
// We verified the signature, but does the user still exist?
// Has their password changed since the token was issued?
const user = await this.users.findById(payload.sub);
if (!user) throw new UnauthorizedException();
return user; // Attached to request.user
}
private extractToken(req: Request): string | null {
const header = req.headers.get("Authorization");
if (header?.startsWith("Bearer ")) {
return header.substring(7);
}
return null;
}
}Protecting Routes (Authorization)
Once the strategy is defined, you use the standard UseGuards decorator combined with the plugin's AuthGuard factory.
import { Controller, UseGuards, Get } from "@project-karin/core";
import { AuthGuard, CurrentUser } from "@project-karin/auth";
@Controller("profile")
@UseGuards(AuthGuard("jwt")) // 🔒 Rejects unauthenticated requests
export class ProfileController {
@Get()
getProfile(@CurrentUser() user: User) {
// The 'user' here is exactly what you returned from `validate()`
return user;
}
}
> [!NOTE] Handling CORS Preflight
> The `AuthGuard` automatically ignores `OPTIONS` requests. This prevents CORS issues where browsers send preflight requests without the Authorization header. You do not need to manually exclude `OPTIONS` from your guards.Stateless vs Stateful
The AuthPlugin leverages @project-karin/auth, which is built on the Web Crypto API. This is a deliberate design choice to ensure Serverless Compatibility.
- Stateless (JWT): The token contains all necessary info. No session store is needed. This is ideal for Cloudflare Workers or Lambda, where memory is ephemeral.
- Stateful (Sessions): (Coming Soon) If you need server-side invalidation (logout), you would typically pair this with the
RedisPluginto store session IDs.
Dependency Injection for Secrets
Never hardcode secrets. Use the lazy configuration pattern to inject secrets from the ConfigPlugin.
// main.ts
const jwtPlugin = new JwtPlugin({
// The function executes at init time
secret: () => config.get("JWT_SECRET"),
signOptions: { expiresIn: "15m" } // Short-lived access tokens
});Refresh Token Rotation
For a robust security architecture, do not set long expirations on Access Tokens. Instead, implement a Refresh Token flow.
- Access Token (JWT): Short life (15 min). Used for API calls.
- Refresh Token (Opaque): Long life (7 days). Stored in a simpler database table or Redis.
Your AuthController would have a /refresh endpoint that verifies the opaque token against the DB and issues a new JWT pair.
