Authorization
createAuthzInterceptor enforces access control after authentication using code-based rules and an optional fallback callback. Use createProtoAuthzInterceptor when policy is declared in proto options.
Declarative Rules
Define rules as an ordered list. The first rule whose method pattern and requirements both match wins. A rule whose role or scope requirements fail is skipped; it does not deny the request by itself.
import { createAuthzInterceptor } from '@connectum/auth';
const authz = createAuthzInterceptor({
defaultPolicy: 'deny',
rules: [
{ name: 'admin-only', methods: ['admin.v1.AdminService/*'], requires: { roles: ['admin'] }, effect: 'allow' },
{ name: 'write-scope', methods: ['data.v1.DataService/Write'], requires: { scopes: ['write'] }, effect: 'allow' },
],
});2
3
4
5
6
7
8
9
Rule Fields
| Field | Type | Description |
|---|---|---|
name | string | Rule name for logging and debugging |
methods | readonly string[] | Exact method, Service/*, or * |
requires | { roles?, scopes? } | Required roles and/or scopes |
effect | 'allow' | 'deny' | What to do when the rule matches. Exactly these two lowercase strings; any other value ('DENY', 'block') throws a TypeError when the interceptor is created |
Matching Semantics
- Roles use any-of semantics -- the user needs at least one of the listed roles.
- Scopes use all-of semantics -- the user needs all listed scopes.
- Rules without
requiresmatch all authenticated users for the named methods. - An empty or omitted list places no restriction on that dimension:
requires: { roles: [] }does not restrict roles, andrequires: {}matches any authenticated caller, exactly like a rule withoutrequires. A rule restricts a dimension only when its list has at least one entry. - This interceptor requires an
AuthContextbefore evaluating any rule. A rule namedpublicdoes not bypass authentication. Put intentionally public methods in both authentication and authorizationskipMethodslists.
Method Patterns
| Pattern | Description |
|---|---|
'public.v1.PublicService/*' | All methods of the service |
'*' | All services and methods (allowed in rule methods and in internalMethods) |
'admin.v1.AdminService/DeleteUser' | Exact method match |
Partial wildcards such as Write* or admin.v1.*/* are not supported by auth interceptors. List exact methods or use a complete Service/* pattern. A pattern that is not one of the three forms (a partial wildcard, a bare service name, a leading slash) throws a TypeError that names the option, the rule and the pattern when the interceptor is created, in skipMethods, in a rule's methods and in internalMethods; it used to be accepted and never matched, so a deny rule written that way did nothing.
The catch-all '*' is not accepted in skipMethods. createAuthInterceptor, createJwtAuthInterceptor, createSessionAuthInterceptor, createGatewayAuthInterceptor and createAuthzInterceptor throw when it is created with skipMethods: ['*'], because the option would switch the interceptor off for every method without saying so. To turn an interceptor off, do not register it; to skip a whole service, list 'pkg.v1.Service/*'. See Auth option validation.
Programmatic Callback
For complex logic that can not be expressed as rules, add an authorize callback. It is invoked only when no rule matches:
const authz = createAuthzInterceptor({
defaultPolicy: 'deny',
rules: [],
authorize: (context, req) => context.roles.includes('superadmin'),
});2
3
4
5
If authorize returns (or resolves to) exactly true, the request is allowed. Any other result fails the request with Code.PermissionDenied, including when defaultPolicy is allow: false, but also undefined and a truthy value that is not a boolean, such as the string 'false' that (ctx) => ctx.claims.isAdmin returns for a string claim. Write the comparison out, for example ctx.claims.isAdmin === true. The default policy applies only when no rule matches and no callback is configured.
defaultPolicy is 'allow' or 'deny' and defaults to 'deny' when omitted. Any other value ('Deny', '', null) throws a TypeError when the interceptor is created, so a typo cannot open the methods that no rule matches. See Auth authorization and JWT validation when upgrading.
Proto-Based Authorization
For defining authorization rules directly in .proto files using custom options, see the dedicated Proto-Based Authorization page. Proto options are read at runtime by createProtoAuthzInterceptor() and take priority over programmatic rules.
Interceptor Chain Position
Auth and authz interceptors must be placed after errorHandler and before resilience interceptors:
This ensures:
- Authentication errors are properly formatted by
errorHandler - Unauthenticated requests are rejected before consuming resilience resources
- Auth context is available to all downstream interceptors
const server = createServer({
services: [routes],
interceptors: [
createErrorHandlerInterceptor(),
jwtAuth, // immediately after errorHandler
authz, // immediately after authentication
...createDefaultInterceptors({ errorHandler: false }),
],
});2
3
4
5
6
7
8
9
Full Example
import { createServer } from '@connectum/core';
import { createDefaultInterceptors, createErrorHandlerInterceptor } from '@connectum/interceptors';
import { createJwtAuthInterceptor, createAuthzInterceptor } from '@connectum/auth';
const publicMethods = ['public.v1.PublicService/*'];
const jwtAuth = createJwtAuthInterceptor({
jwksUri: 'https://auth.example.com/.well-known/jwks.json',
issuer: 'https://auth.example.com/',
skipMethods: publicMethods,
});
const authz = createAuthzInterceptor({
defaultPolicy: 'deny',
skipMethods: publicMethods,
rules: [
{ name: 'admin-only', methods: ['admin.v1.AdminService/*'], requires: { roles: ['admin'] }, effect: 'allow' },
{ name: 'write-scope', methods: ['data.v1.DataService/Write'], requires: { scopes: ['write'] }, effect: 'allow' },
],
authorize: (context, req) => {
// Fallback: superadmins can do anything
return context.roles.includes('superadmin');
},
});
const server = createServer({
services: [routes],
// Recommended order: errorHandler -> AUTH -> AUTHZ -> rest.
interceptors: [
createErrorHandlerInterceptor(),
jwtAuth,
authz,
...createDefaultInterceptors({ errorHandler: false }),
],
});
await server.start();2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
Verify
Call the admin method with an admin role and without it, then repeat without credentials. For the example above, the admin succeeds, a caller with neither admin nor the fallback superadmin role receives Code.PermissionDenied, and the unauthenticated caller receives Code.Unauthenticated. Call a public method without credentials to check that both skip lists agree.
If an allow rule is never reached, check the exact service/method names and its requirements. If a later fallback unexpectedly grants access, remember that failing a rule's requirements skips that rule rather than denying immediately.
Related
- Auth Overview -- all authentication strategies
- JWT Authentication -- token verification
- Proto-Based Authorization -- declarative authz via
.protooptions - Auth Context -- accessing identity in handlers
- Method Filtering -- per-method interceptor routing
- @connectum/auth -- Package Guide
- @connectum/auth API -- Full API Reference
- ADR-024: Auth/Authz Strategy -- design rationale