Authentication

JWT, global guards, public routes, generic OAuth2, and API Key.

The authentication module is optional. With JWT, the template includes a User entity, email/password login, and RS256 token issuance. With OAuth2, users are created or reused after the authorization code flow. API Key authenticates machine-to-machine calls at the HTTP edge.

Installing auth does not patch data-source-factory.ts: the User entity joins the DataSource via @Entity (DbContext), and UserRepository is registered in RepositoryModule.

Piece Role
SecurityModule Configures RS256 JWT, Passport, and token/OAuth2 services
AuthGuard Global guard — validates Bearer JWT and/or ApiKey header
ProfilesGuard Global guard — restricts by token profile
@IsPublic() Marks routes that bypass AuthGuard
@RestrictionByProfile([AuthProfile.admin]) Restricts endpoint to listed profiles
ILoggedUserInfoService Request-scoped service for handlers/controllers
AuthProfile (src/core/auth/auth-profile.enum.ts) String enum with supported profiles (user, admin)
POST /auth/login Email/password login; issues access/refresh pair
GET /auth/user-info Authenticated user data
POST /auth/refresh Renews token pair using refresh token (Bearer or cookie)

Routes with @IsPublic() bypass AuthGuard and do not require Bearer in OpenAPI/Scalar:

typescript
import { IsPublic } from '@/host/decorators/is-public.decorator';

@Post('login')
@IsPublic()
handle() { ... }

All other endpoints are protected by default — no need for @ApiBearerAuth() on each controller.

The profile value comes from the user in the database (loaded by AuthGuard after JWT validation):

typescript
import { AuthProfile } from '@/core/auth/auth-profile.enum';
import { RestrictionByProfile } from '@/host/decorators/restriction-by-profile.decorator';

@Delete(':id')
@RestrictionByProfile([AuthProfile.admin])
handle(@Param('id') id: string) { ... }

Public endpoint to authenticate with email and password:

bash
POST /auth/login
Content-Type: application/json

{
  "username": "admin@example.com",
  "password": "admin123"
}

Response:

json
{
  "accessToken": "...",
  "refreshToken": "..."
}

Renew the access/refresh pair without re-authenticating:

bash
POST /auth/refresh
Authorization: Bearer <refreshToken>

Or send the refresh token as an httpOnly cookie named refreshTokenAuthGuard promotes it to Authorization automatically on this route.

On login, the cookie is set with httpOnly and path=/. On localhost (API_HOST containing localhost): SameSite=Strict without Secure. Outside localhost (front ≠ API): SameSite=None; Secure so the browser accepts Set-Cookie on cross-site XHR.

Response format matches POST /auth/login (accessToken + refreshToken). Refresh tokens are rejected on all other routes by JwtStrategy.

Inject ILoggedUserInfoService (same pattern as Globo Seguros / Solicita.ai):

typescript
import { ILoggedUserInfoService } from '@/domain/services/ilogged-user-info.service';

@Injectable()
export class MyHandler {
  constructor(private readonly loggedUserInfo: ILoggedUserInfoService) {}

  async handle(req: MyRequest) {
    const user = this.loggedUserInfo.getUser();
  }
}

The service is request-scoped and reads request.user populated by AuthGuard after JWT validation.

With the CLI (kl-nest newOAuth2), the template ships a ready authorization code flow. The usual case is login with third-party providers (Google, Microsoft, Auth0, Keycloak, GitHub Enterprise, Okta, etc.) — you only fill credentials in .env. You do not reimplement code exchange, CSRF state, OIDC discovery, or controllers.

Google and Microsoft in .env.example are just examples. The library is generic: list as many providers as you need in OAUTH2_PROVIDERS and repeat the OAUTH2_{KEY}_* pattern for each. The KEY is the value you send in the body (provider: "auth0"OAUTH2_AUTH0_*).

Piece Role
OAuthProviderRegistry Reads N providers from OAUTH2_PROVIDERS + OAUTH2_{KEY}_* variables
OAuth2AuthService Generates state, builds auth URL, exchanges code, fetches userinfo
POST /oauth2/auth-link Returns the authorization URL for the given provider
POST /oauth2/token Exchanges code + stateOAuthUserInfoDto
Scalar One OAuth2 scheme per listed provider at /doc

Created outside the API, in the IdP console:

Data Where to get it
OAUTH2_PROVIDERS Comma-separated list — as many providers as you need
OAUTH2_{KEY}_CLIENT_ID / _CLIENT_SECRET Provider console (Google Cloud, Azure, Auth0, …)
OAUTH2_{KEY}_DOMAIN Provider OIDC issuer (automatic discovery)
OAUTH2_{KEY}_SCOPE Scopes required by the provider
Registered redirect_uri API_HOST + /oauth2/callback (or OAUTH2_{KEY}_REDIRECT_PATH)

For each key in OAUTH2_PROVIDERS, add the OAUTH2_{KEY}_* block. Endpoints (authorization, token, userinfo) come from /.well-known/openid-configuration.

env
OAUTH2_PROVIDERS=google,microsoft,auth0,keycloak
# --- google (example) ---
OAUTH2_GOOGLE_DOMAIN=https://accounts.google.com
OAUTH2_GOOGLE_CLIENT_ID=...
OAUTH2_GOOGLE_CLIENT_SECRET=...
OAUTH2_GOOGLE_SCOPE=openid profile email
# --- microsoft (example) ---
OAUTH2_MICROSOFT_DOMAIN=https://login.microsoftonline.com/common/v2.0
OAUTH2_MICROSOFT_CLIENT_ID=...
OAUTH2_MICROSOFT_CLIENT_SECRET=...
OAUTH2_MICROSOFT_SCOPE=openid profile email
# --- auth0 ---
OAUTH2_AUTH0_DOMAIN=https://tenant.auth0.com
OAUTH2_AUTH0_CLIENT_ID=...
OAUTH2_AUTH0_CLIENT_SECRET=...
OAUTH2_AUTH0_SCOPE=openid profile email
# --- keycloak ---
OAUTH2_KEYCLOAK_DOMAIN=https://idp.company.com/realms/prod
OAUTH2_KEYCLOAK_CLIENT_ID=...
OAUTH2_KEYCLOAK_CLIENT_SECRET=...
OAUTH2_KEYCLOAK_SCOPE=openid profile email
API_HOST=http://localhost:3000

End-to-end flow (any provider):

mermaid
sequenceDiagram
  participant FE as Frontend
  participant API as Koala Nest
  participant IdP as OIDC Provider
  FE->>API: POST /oauth2/auth-link { provider: auth0 }
  API-->>FE: { url }
  FE->>IdP: redirect (user signs in)
  IdP-->>FE: callback ?code&state
  FE->>API: POST /oauth2/token { provider, code, state }
  API-->>FE: OAuthUserInfoDto
  FE->>API: POST /auth/login { username, password }
  API-->>FE: accessToken + refreshToken

When you host the server and it does not expose OIDC discovery, set URLs manually (no _DOMAIN):

env
OAUTH2_PROVIDERS=myapp
OAUTH2_MYAPP_CLIENT_ID=...
OAUTH2_MYAPP_CLIENT_SECRET=...
OAUTH2_MYAPP_SCOPE=openid profile email
OAUTH2_MYAPP_AUTHORIZATION_URL=https://auth.myapp.com/oauth/authorize
OAUTH2_MYAPP_TOKEN_URL=https://auth.myapp.com/oauth/token
OAUTH2_MYAPP_USERINFO_URL=https://auth.myapp.com/oauth/userinfo

On POST /oauth2/auth-link, the API generates a random state and stores it temporarily:

text
oauth2:state:{state} → { provider }   (10 min TTL)

On POST /oauth2/token, it checks the state exists, matches the request provider, and removes the key (one-time use). This ensures the code belongs to a flow started by the API — CSRF protection. The frontend (Angular, etc.) only forwards code and state; validation is always server-side, because the endpoint is public.

Implemented in OAuth2AuthService — uses ICacheService under the hood (temporary storage, not business data cache).

Scenario Behavior
Single instance (local dev) state stays in memory (InMemoryCacheService) — Redis not required
Multiple instances (load balancer, K8s) Recommended REDIS_CONNECTION_STRINGauth-link may run on replica A and token on B
mermaid
sequenceDiagram
  participant FE as Frontend
  participant API as Koala Nest
  participant Store as Store (memory or Redis)
  participant IdP as OIDC Provider
  FE->>API: POST /oauth2/auth-link { provider }
  API->>Store: save oauth2:state:{state}
  API-->>FE: { url with state }
  FE->>IdP: redirect
  IdP-->>FE: callback ?code&state
  FE->>API: POST /oauth2/token { provider, code, state }
  API->>Store: validate and remove state
  API-->>FE: OAuthUserInfoDto

The template does not persist users or issue API JWT automatically after OAuth. You decide:

  1. Map OAuthUserInfoDto → claims (sub, profile, email);
  2. Call POST /auth/login to issue the API JWT;
  3. (Optional) create/update the user in the database before step 2.

When the CLI installs authentication, main.ts registers global guards. Background jobs are started automatically by JobsBootstrapService via JobsModule.register() in AppModule:

typescript
app.useGlobalGuards(
  await app.resolve(AuthGuard),
  await app.resolve(ProfilesGuard),
);

Job bootstrap subscribes to domain events and starts CronJobs only when CRON_JOBS_ENABLED=true. The delay before starting jobs is controlled by BOOTSTRAP_DELAY_MS.

API Key authenticates synchronous HTTP callers (integrations, BFF, occasional service hops). It does not replace a message broker or force a file-proxy API — cloud storage + messaging remain the preferred scalable design. The strategy lives in the host layer; handlers only see the already-authenticated user via ILoggedUserInfoService.

CLI (new): --auth jwt,api-key (or oauth2,api-key / jwt,oauth2,api-key). On an existing project: kl-nest add auth api-key (with JWT and/or OAuth2 already installed, or in the same command: kl-nest add auth jwt api-key). Optional --api-key-internal-subnet allows private (RFC1918) IPs for domain type when pods talk inside the cluster.

  • Endpoints under /api-key (create/list/read/update/delete), scoped to the authenticated user
  • On create, the key is an RS256 JWT with typ: api-key, sub = userId, iss = key id — returned only in that response
  • Usage: header ApiKey: <jwt>
Type Validation
host req.hostname in the list
uri hostname + path (without route params)
domain Client IP vs registered IP or domain (reverse DNS / A/AAAA lookup). No Origin/Referer headers

* in the list only unlocks in develop/test. With internal subnet enabled, private IPs also pass domain without listing every pod.

Enable trust proxy when the API sits behind a load balancer / ingress.

With authentication installed, Scalar obtains credentials via authentication in apiReference:

  • JWT: JWT scheme (password flow) → POST /auth/login
  • OAuth2: one scheme per provider (authorization code) → POST /oauth2/scalar-token
  • API Key: ApiKey scheme (header ApiKey)

Full guide: OpenAPI with Scalar