Authorization -- Error Reference
Complete error messages and troubleshooting for the authorization module. See Setup & Configuration for initial setup.
Error Flow Diagram
Complete Error Reference
All error messages from the authorization module, organized by source. Status is what error.statusCode holds - most calls use getError({ message }) with no explicit statusCode, which the shared ApplicationError defaults to 400. Only the request-pipeline errors below (steps 2, 4, 7) set an explicit 401/403.
Component Errors (AuthorizeComponent)
Thrown during binding(), at application startup.
| Error Message | Status | Method | Cause |
|---|---|---|---|
[AuthorizeComponent] No authorize options found. Bind options to AuthorizeBindingKeys.OPTIONS before registering the component. | 400 | binding | IAuthorizeOptions was never bound before this.component(AuthorizeComponent) |
Enforcer Registry Errors (AuthorizationEnforcerRegistry + inherited AbstractAuthRegistry)
| Error Message | Status | Method | Cause |
|---|---|---|---|
[getKey] Invalid name | name: {{name}} | 400 | getKey | Enforcer name is empty or falsy |
[AuthorizationEnforcerRegistry] No items registered | 400 | getDefaultName | getDefaultEnforcerName() called with zero enforcers registered |
[AuthorizationEnforcerRegistry] Duplicate enforcer name(s): {{names}} | 400 | register | Two or more enforcers in the same register() call share a name |
[AuthorizationEnforcerRegistry] Enforcer already registered: {{name}} | 400 | register | An enforcer with this name was already registered in a previous call |
[AuthorizationEnforcerRegistry] Descriptor not found: {{name}} | 400 | resolveDescriptor | enforcerName doesn't match any registered enforcer |
[AuthorizationEnforcerRegistry] Failed to resolve: {{name}} | 400 | resolveDescriptor | The registered class has unsatisfied @inject dependencies |
[AuthorizationEnforcerRegistry] Enforcer "{{name}}" does not support cache invalidation | 400 | invalidateUserCache / rebuildUserCache | The resolved enforcer doesn't implement the optional cache-management methods |
NOTE
[AuthorizationEnforcerRegistry] No items registered can only surface if getDefaultEnforcerName() is called directly. During normal middleware execution, the provider checks registry.hasEnforcers() first and skips authorization when no enforcers exist - it never reaches this throw.
Authorization Provider Errors (AuthorizationProvider - the request pipeline)
The only errors in the module with an explicit statusCode.
| Error Message | Status | Pipeline step | Cause |
|---|---|---|---|
Authorization failed: No authenticated user found | 401 | 2 - User check | Authentication.CURRENT_USER is missing from the Hono context |
Authorization denied by voter | action: {{action}} | resource: {{resource}} | 403 | 4 - Voters | A voter function returned AuthorizationDecisions.DENY |
Authorization failed: user.principalType is required for enforcer-based authorization | 400 | 6 - Build rules | The authenticated user object has no principalType field |
Authorization denied | action: {{action}} | resource: {{resource}} | 403 | 7 - Evaluate | The enforcer returned DENY, or ABSTAIN and defaultDecision resolved to 'deny' |
Casbin Enforcer Errors - Startup (CasbinAuthorizationEnforcer.configure())
| Error Message | Status | Method | Cause |
|---|---|---|---|
[CasbinAuthorizationEnforcer] "casbin" is not installed | 400 | configure | The optional casbin peer dependency isn't installed |
[CasbinAuthorizationEnforcer] options.model is required. | 400 | configure | model is missing from the enforcer options |
[registerMatchers] Role definition "{{name}}" is not declared in the Casbin model. Declare it under [role_definition] (e.g. | 400 | configure (via registerMatchers) | domainMatching.roleDefinition isn't declared in [role_definition] (only checked when domainMatching is set) |
[CasbinAuthorizationEnforcer] Matcher smoke test failed at warmup - the model matcher did not compile ... {{error}} | 400 | configure (via assertMatcherCompilesSync) | The matcher expression doesn't compile: syntax error, an unregistered function, or an arity mismatch |
[CasbinAuthorizationEnforcer] cached.options.expiresIn must be >= 10000 (ms) | Received: {{value}} | 400 | configure (via validateExpiresIn) | cached.options.expiresIn is below MIN_EXPIRES_IN (10,000 ms) |
[resolveDomainMatchingFn] Unsupported func: {{name}} | Valids: [...] | 400 | configure (via registerMatchers) | domainMatching.fn isn't a CasbinDomainMatchingFunctions value |
[resolveModel] Invalid model.driver | Valids: [file, text] | 400 | configure (via resolveModel) | model.driver isn't 'file' or 'text' |
NOTE
There is no Invalid cached.driver runtime error - cached is a typed discriminated union ({ use: false } | { use: true; driver: 'redis'; ... }), so an unsupported cache driver is a compile-time error. Caching is Redis-only.
Casbin Enforcer Errors - Runtime (buildRules / evaluate / cache management)
| Error Message | Status | Method | Cause |
|---|---|---|---|
[CasbinAuthorizationEnforcer] Not configured. Call configure() first. | 400 | evaluate | evaluate() ran before configure() built the enforcer pool - should not happen via resolveEnforcer() |
[CasbinAuthorizationEnforcer] request.action and request.resource are required. | 400 | evaluate | Malformed IAuthorizationRequest passed to evaluate() |
[CasbinAuthorizationEnforcer] keyFn returned an empty cache key. | 400 | resolveCacheKey (via buildRules, invalidateUserCache, rebuildUserCache) | cached.options.keyFn returned a falsy value for this user |
[CasbinAuthorizationEnforcer] Cache management requires the redis cache driver, but caching is disabled. | 400 | invalidateUserCache / rebuildUserCache (via requireRedisCache) | Called with cached: { use: false } |
[extractUserLines] Adapter does not support loadFilteredPolicy. | 400 | buildRules (via extractUserLines) | options.adapter doesn't implement casbin's FilteredAdapter.loadFilteredPolicy |
[loadPolicyLinesIntoModel] Not configured. Call configure() first. | 400 | evaluate (via loadPolicyLinesIntoModel) | Same root cause as the evaluate "Not configured" row, different call site |
Not an error - [CasbinAuthorizationEnforcer] Cached payload is not an array of policy lines. is logged at warn level by parseCachedPolicyLines and never thrown up to the caller: a corrupted or legacy Redis entry is discarded and the lines are refetched from the adapter, so the request never 500s (or 400s) on cache corruption.
Troubleshooting
"[AuthorizeComponent] No authorize options found"
Cause: AuthorizeComponent was registered but IAuthorizeOptions was not bound to the container.
Fix: Bind options before registering the component.
this.bind<IAuthorizeOptions>({ key: AuthorizeBindingKeys.OPTIONS }).toValue({
defaultDecision: 'deny',
alwaysAllowRoles: ['999_super-admin'],
});
this.component(AuthorizeComponent);"Authorization failed: No authenticated user found"
Cause: The authorization middleware runs after authentication, but no user was found on the context (Authentication.CURRENT_USER is undefined). Common triggers: the route has authorize but no authenticate, authentication was skipped but authorization wasn't, or the request genuinely has no valid credentials.
Fix: Every route with authorize needs a matching authenticate config.
this.defineRoute({
configs: {
path: '/',
method: 'get',
authenticate: { strategies: [Authentication.STRATEGY_JWT] }, // must be present
authorize: { action: AuthorizationActions.READ, resource: 'Article' },
// ...
},
handler: async context => { /* ... */ },
});"Authorization failed: user.principalType is required"
Cause: The authenticated user object has no principalType field. Enforcer-based authorization uses it to build the casbin subject (e.g. User_123).
Fix: Set principalType when you build the user in your authentication service or token payload.
return {
userId: '123',
principalType: 'User', // required for authorization
roles: [...],
};NOTE
principalType is read via the IAuthUser index signature, not a dedicated field - it must be a real property on the returned user object.
"Authorization denied by voter | action: ... | resource: ..."
Cause: A voter function explicitly returned AuthorizationDecisions.DENY.
Fix: Inspect the voter named in your logs. Common causes: ownership check failed, a time-window check rejected the request, or a resource-state check (locked/archived) blocked it.
"Authorization denied | action: ... | resource: ..."
Cause: enforcer.evaluate() returned DENY, or ABSTAIN and it fell back to defaultDecision. Usually: no matching policy for the user, a matching deny policy, or the model doesn't cover the requested domain/resource/action combination.
Fix: Work through this checklist:
- Policies are loaded correctly - verify the adapter returns the right rows for this user (
ScopedCasbinAdapterreadsPolicyDefinitionfiltered bysubject_type/subject_id). - Subject format matches.
normalizePayloadFn(or the default scoped payload) must produce a subject that matches what's stored, e.g.User_123. - The model covers the request. For a custom (non-scoped)
.conf, confirm the matcher handles the action/resource/domain shape you're sending. - Set
defaultDecisionexplicitly - don't rely on an implicit fallback:
this.bind<IAuthorizeOptions>({ key: AuthorizeBindingKeys.OPTIONS }).toValue({
defaultDecision: 'deny', // explicit is better than implicit
});"[AuthorizationEnforcerRegistry] Duplicate enforcer name(s): ..."
Cause: Two or more enforcers in the same register() call have the same name.
Fix: Give every enforcer in the call a unique name.
AuthorizationEnforcerRegistry.getInstance().register({
container: this,
enforcers: [
{ enforcer: CasbinAuthorizationEnforcer, name: 'casbin', type: 'casbin', options: { /* ... */ } },
{ enforcer: MyCustomEnforcer, name: 'custom', type: 'custom' }, // different name
],
});"[AuthorizationEnforcerRegistry] Enforcer already registered: ..."
Cause: An enforcer with this name was already registered in a previous register() call - the registry doesn't allow overwriting.
Fix: Register each name once. Call AuthorizationEnforcerRegistry.getInstance().reset() first if you genuinely need to re-register (typically only in tests).
"[AuthorizationEnforcerRegistry] Descriptor not found: ..."
Cause: enforcerName in an authorize() call doesn't match any registered enforcer name.
Fix: Match the name used in register(). The default (when enforcerName is omitted) is the first one registered.
"[AuthorizationEnforcerRegistry] Failed to resolve: ..."
Cause: The enforcer class is registered, but DI resolution returned null - typically an unsatisfied @inject dependency in its constructor.
Fix: Confirm every @inject in the enforcer's constructor is bound in the same container before resolveEnforcer() runs.
"[CasbinAuthorizationEnforcer] "casbin" is not installed"
Cause: The Casbin enforcer dynamically imports casbin in configure(), but the package isn't installed.
Fix:
bun add casbin"[CasbinAuthorizationEnforcer] options.model is required."
Cause: The enforcer options have no model.
Fix: Provide a model - inline text (recommended: CASBIN_RBAC_DOMAIN_SCOPED_MODEL) or a file path.
options: {
model: { driver: CasbinEnforcerModelDrivers.TEXT, definition: CASBIN_RBAC_DOMAIN_SCOPED_MODEL },
isScoped: true,
adapter,
cached: { use: false },
}"[registerMatchers] Role definition "g2" is not declared in the Casbin model..."
Cause: domainMatching.roleDefinition references a relation the model doesn't declare under [role_definition]. Casbin would otherwise register the function as a silent no-op, leaving wildcard domains permanently unmatched (global roles silently denied) - the enforcer throws at boot instead.
Fix: Point roleDefinition at a relation the model actually declares.
domainMatching: { roleDefinition: 'g', fn: CasbinDomainMatchingFunctions.KEY_MATCH } // model must declare `g = _, _, _`"[CasbinAuthorizationEnforcer] Matcher smoke test failed at warmup..."
Cause: assertMatcherCompilesSync() runs a dummy enforceSync() at warmup to force casbin's lazy matcher compile. It failed - a matcher syntax error, a function referenced in the matcher that was never registered, or a request-arity mismatch (3 vs. 4 tokens).
Fix: Check the [matchers] section of your .conf. If scoped, confirm isScoped: true is set (registers objectMatch + keyMatch automatically); if using a custom flat model, confirm every function the matcher calls is registered via domainMatching.
"[CasbinAuthorizationEnforcer] cached.options.expiresIn must be >= 10000"
Cause: expiresIn is below the 10,000 ms minimum (MIN_EXPIRES_IN).
Fix:
cached: {
use: true,
driver: 'redis',
options: {
connection: redisHelper,
expiresIn: 5 * 60 * 1000, // 5 minutes (minimum: 10,000 ms)
keyFn: ({ user }) => `authz:policies:${user.principalType}:${user.userId}`,
},
},"[CasbinAuthorizationEnforcer] Not configured. Call configure() first."
Cause: evaluate() (or its internal loadPolicyLinesIntoModel()) ran before configure() built the enforcer pool. This shouldn't happen through resolveEnforcer() (which auto-configures), but can occur if the enforcer is resolved manually from the DI container.
Fix: Always go through the registry, which handles configure-once automatically.
const enforcer = await AuthorizationEnforcerRegistry.getInstance().resolveEnforcer({ name: 'casbin' });"[CasbinAuthorizationEnforcer] keyFn returned an empty cache key."
Cause: cached.options.keyFn returned a falsy value for a user.
Fix: Always return a stable, non-empty key.
keyFn: ({ user }) => `authz:policies:${user.principalType}:${user.userId}`,"[extractUserLines] Adapter does not support loadFilteredPolicy"
Cause: options.adapter doesn't implement casbin's FilteredAdapter.loadFilteredPolicy. Authorization always loads policies filtered by principal.
Fix: Use ScopedCasbinAdapter, or extend BaseFilteredAdapter and implement loadFilteredPolicy.
import { ScopedCasbinAdapter } from '@venizia/ignis';
const adapter = new ScopedCasbinAdapter({ dataSource, entities: { /* IScopedCasbinEntities */ } });"[resolveModel] Invalid model.driver | Valids: [file, text]"
Cause: An unsupported model.driver string was passed.
Fix: Use CasbinEnforcerModelDrivers.FILE ('file') or CasbinEnforcerModelDrivers.TEXT ('text').
"[CasbinAuthorizationEnforcer] Cache management requires the redis cache driver, but caching is disabled."
Cause: invalidateUserCache() or rebuildUserCache() was called on an enforcer configured with cached: { use: false }.
Fix: Either configure Redis caching, or don't call the cache-management methods for a non-cached enforcer.
Common Patterns
Authorization is not running
Check, in order:
authorizeis actually set on the route config (not justauthenticate).authenticateisn't{ skip: true }- that skips authorization too, in both raw route configs and the CRUD factory.- The component is registered AND at least one enforcer is registered via
AuthorizationEnforcerRegistry. - If no enforcers are registered, the middleware skips authorization silently (
next()) rather than throwing - this is often the real cause of "authorization does nothing" during rollout.
Rules are rebuilt on every request
Rules cache on Authorization.RULES, but only within one request:
- A new HTTP request always starts with an empty cache.
- Multiple
authorizespecs on the same route share the cache - only the first builds. undefined/nullonAuthorization.RULEStriggers a rebuild -c.set(Authorization.RULES, null)forces one mid-request.
Casbin policies not loading
- Adapter entities -
IScopedCasbinEntities(policyDefinition/permissiontable + schema names,principals,domainTypes) must match your actual database schema. - Variant column -
PolicyDefinition.variantmust useAuthorizationPolicyVariants.*.actionvalues:grant,assign_role,join_domain,role_inherits,resource_inherits,action_inherits,domain_inherits. - Subject/target types - the adapter's SQL filters by
subject_type/target_typeagainstprincipalsanddomainTypes; a mismatch silently returns zero rows. - Model - scoped RBAC needs
CASBIN_RBAC_DOMAIN_SCOPED_MODELwithisScoped: truetogether; one without the other misfires.
Redis cache not working
- Connection - verify the
IRedisHelper(RedisSingleHelper/RedisClusterHelper/RedisSentinelHelper) is actually connected. keyFn- must return a unique, non-empty key per user.expiresIn- must be>= 10_000(MIN_EXPIRES_IN).- Corruption is silent - a malformed cached payload is discarded and refetched (see the Casbin runtime error table above), so a "cache never seems to hit" symptom is more likely a
keyFnmismatch than corruption.
See Also
- Setup & Configuration -- Binding keys, options interfaces, and initial setup
- Usage & Examples -- Securing routes, voters, patterns, and CRUD integration
- API Reference -- Architecture, enforcer internals, provider, registry, and adapters