Skip to main content

Authentication

info

This page is the Netron-layer reference for AuthConfig, PolicyEngine, and the BuiltInPolicies library. For the end-to-end authorisation surface (permission grammar, per-user overrides, RLS bridge, audit), start at Authentication & Authorisation. For the higher-level façade decorators that compile to the @Auth({policies}) form below, see Permissions → Scopes.

Netron separates authentication (who is the caller?) from authorisation (is this caller allowed to do this?). Two managers, one shared policy engine, composable policies.

Import path:

import {
AuthenticationManager,
AuthorizationManager,
PolicyEngine,
BuiltInPolicies,
} from '@omnitron-dev/titan/netron/auth';

The two managers

ManagerJob
AuthenticationManagerValidate credentials, produce an auth context
AuthorizationManagerEvaluate policies against the auth context

These are wired by the titan-auth ecosystem module (or by your own integration). You rarely construct them directly.

Auth context

Every authenticated call carries an auth context with:

{
userId: string;
roles: string[];
permissions: string[];
scopes: string[];
// …integration-specific extras
}

This is what AuthConfig policies match against.

@Auth(config) — configuring a method

AuthConfig (from @omnitron-dev/titan/decorators):

interface AuthConfig {
roles?: string[]; // ANY role grants access
permissions?: string[]; // ALL required
scopes?: string[]; // ALL OAuth2 scopes required
policies?: string[] | { all: string[] } | { any: string[] } | PolicyExpression;
allowAnonymous?: boolean;
inherit?: boolean; // inherit class-level policies
override?: boolean; // override class-level policies
}

Usage:

import { Public, Auth } from '@omnitron-dev/titan/decorators';

@Service('orders@1.0.0')
class OrdersService {
@Public()
@Auth({ scopes: ['orders:read'] })
async list() { /* … */ }

@Public()
@Auth({ roles: ['admin'] })
async deleteAll() { /* … */ }

@Public()
@Auth({ allowAnonymous: true }) // explicit anonymous
async ping() { /* … */ }

@Public()
@Auth({ policies: { any: ['policy:admin', 'policy:resource-owner'] } })
async modifyResource(id: string) { /* … */ }
}

The same configuration is also accepted inline through @Public:

@Public({ auth: { scopes: ['orders:read'] } })
async list() { /* … */ }

Pick one style per project.

BuiltInPolicies — reusable policy definitions

The BuiltInPolicies namespace produces policy definitions you can register with the policy engine:

import { BuiltInPolicies } from '@omnitron-dev/titan/netron/auth';

const policies = [
BuiltInPolicies.requireRole('admin'),
BuiltInPolicies.requireAnyRole(['admin', 'support']),
BuiltInPolicies.requireAllRoles(['user', 'verified']),
BuiltInPolicies.requirePermission('users:read'),
// …additional helpers in the source
];

// Register them with the engine:
policyEngine.register(policies);

Reference by name in AuthConfig:

@Auth({ policies: ['role:admin'] })
async deleteAll() { /* … */ }

The policy name follows the convention shown in the helper (role:admin, role:any:admin,support, etc.) — the registration sets the name based on the helper's logic.

Custom policies

A policy is a PolicyDefinition:

import type { PolicyDefinition } from '@omnitron-dev/titan/netron/auth';

const IsResourceOwner: PolicyDefinition = {
name: 'resource:owner',
description: 'Caller must own the resource referenced by the first argument',
evaluate: async (context) => {
const resourceId = context.args?.[0];
const resource = await context.deps.resolve('OrdersRepo').findById(resourceId);
const allowed = resource?.userId === context.auth?.userId;
return {
allowed,
reason: allowed ? 'Owner' : 'Not owner',
};
},
};

policyEngine.register([IsResourceOwner]);

// Then in your service:
@Public()
@Auth({ policies: ['resource:owner'] })
async getOrder(orderId: string) { /* … */ }

The exact PolicyContext and helper APIs live in netron/auth/policy-engine.ts and netron/auth/types.ts.

Class-level vs method-level

Both work; method-level overrides class-level for that method:

@Service('orders@1.0.0')
@Auth({ scopes: ['orders:*'] }) // class-level default
class OrdersService {
@Public() // inherits class-level
async list() { /* … */ }

@Public()
@Auth({ allowAnonymous: true }) // override
async listPublic() { /* … */ }

@Public()
@Auth({ roles: ['admin'] }) // override
async deleteAll() { /* … */ }
}

Use inherit / override flags in AuthConfig to control combination semantics for fine-grained cases.

Failure semantics

FailureTitanError typeStatus
No credentialsAuthError401
Invalid tokenAuthError401
Authenticated but policy deniedPermissionError403

Both classes extend HttpError extends TitanError. The client can instanceof AuthError vs instanceof PermissionError to discriminate.

Anti-patterns

  • Auth checks in method bodies. Defeats the policy framework. Use @Auth(...). Inline checks duplicate the policy and drift from the rest of the codebase.
  • Single super-scope ('admin'). A flat scope model collapses under growth. Prefer hierarchical scopes (orders:*, orders:read, orders:write) so you can grant least privilege.
  • Auth as the only check. Auth says who can call. It does not say whether the input is valid or whether the resource is in the right state. Combine with @Validate and domain checks.

→ Next: Streaming.