Skip to content

Add method-level authorization via [NexusAuthorize<TPermission>]#65

Merged
DJGosnell merged 10 commits intomasterfrom
feature/method-authorization
Mar 16, 2026
Merged

Add method-level authorization via [NexusAuthorize<TPermission>]#65
DJGosnell merged 10 commits intomasterfrom
feature/method-authorization

Conversation

@DJGosnell
Copy link
Member

@DJGosnell DJGosnell commented Feb 21, 2026

Summary

Adds [NexusAuthorize<TPermission>] attribute support for authorization on server nexus methods and synchronized collections. The source generator emits auth guards in InvokeMethodCore that call a virtual OnAuthorize method before any argument deserialization or method/collection execution. Users define a permission enum, decorate server nexus methods or collection properties with [NexusAuthorize<TPermission>(...)], and override OnAuthorize for custom authorization logic.

Method example:

[NexusAuthorize<Permission>(Permission.Admin)]
public override ValueTask AdminMethod() { ... }

protected override ValueTask<AuthorizeResult> OnAuthorize(
    ServerSessionContext<TProxy> context, int methodId,
    string methodName, ReadOnlyMemory<int> requiredPermissions)
{
    // Custom auth logic
}

Collection example:

public partial interface IServerNexus
{
    [NexusCollection(NexusCollectionMode.ServerToClient)]
    [NexusAuthorize<Permission>(Permission.Admin)]
    INexusList<string> SecureItems { get; }
}

Collection authorization is enforced on every connect/reconnect since all collection connections go through InvokeMethodCore. Reconnecting clients are re-authorized automatically.

Authorization result caching

Authorization results can be cached per-session with configurable TTL at two levels:

// Server-wide default (null = disabled)
serverConfig.AuthorizationCacheDuration = TimeSpan.FromSeconds(30);

// Per-method/collection override (-1 = use server config, 0 = never cache, >0 = seconds)
[NexusAuthorize<Permission>(Permission.Read, CacheDurationSeconds = 60)]  // 60s override
[NexusAuthorize<Permission>(Permission.Admin, CacheDurationSeconds = 0)]  // never cache
[NexusAuthorize<Permission>(Permission.Write)]                             // use server default
Method attribute Server config Result
not set (-1) null no caching
not set (-1) 30s 30s
0 30s no caching (explicit disable)
60 30s 60s (method wins)
10 null 10s (method wins even without server default)

Only Allowed and Unauthorized results are cached. Disconnect and exception paths are never cached. Cache is per-nexus instance (per session) and invalidated automatically on reconnection. Explicit invalidation is available via InvalidateAuthorizationCache().

Reason for the change

NexNet had no built-in authorization mechanism. Users needing per-method or per-collection access control had to implement manual checks, which is error-prone, repetitive, and happens after argument deserialization (wasted work for unauthorized calls). This feature provides a declarative, generator-driven approach with compile-time type safety for both methods and collections.

Impacts of changes

  • New public types: AuthorizeResult enum, NexusAuthorizeAttribute<TPermission>, ProxyUnauthorizedException
  • Modified attribute: NexusAuthorizeAttribute now targets Method | Property (was Method only), gains CacheDurationSeconds property
  • Modified base class: ServerNexusBase<TProxy> gains OnAuthorize virtual, CheckAuthorization protected method, InvalidateAuthorizationCache() overloads, and per-session auth cache
  • Modified config: ServerConfig gains AuthorizationCacheDuration property
  • Wire protocol: New InvocationResultMessage.StateType.Unauthorized (value 3), new MessageType.DisconnectUnauthorized (value 34), new DisconnectReason.Unauthorized
  • Generator: Extracts [NexusAuthorize<>] from class methods and interface collection properties, emits static permission arrays and auth guards for both, adds 4 new diagnostics (NEXNET024-027) covering methods and collections
  • Generator models: CollectionData gains AuthorizeData? field; ExtractAuthorizeData refactored to shared helper supporting both IMethodSymbol and IPropertySymbol
  • Proxy layer: ProxyInvocationBase now handles Unauthorized state by throwing ProxyUnauthorizedException

Migration Steps

None required. This is a purely additive feature. Existing code continues to work without modification. To adopt:

  1. Define a permission enum backed by int (the default) -- non-int underlying types are rejected at compile time via NEXNET027
  2. Add [NexusAuthorize<TPermission>(...)] to server nexus class methods and/or interface collection properties
  3. Override OnAuthorize on the server nexus class
  4. Catch ProxyUnauthorizedException on the client side where needed (methods only -- collections use Disconnect for unauthorized access since they lack a return channel)
  5. Optionally configure ServerConfig.AuthorizationCacheDuration or per-method CacheDurationSeconds for caching

Performance Considerations

  • Zero cost for unannotated methods/collections -- no auth guard is emitted without [NexusAuthorize]
  • Auth check before deserialization -- unauthorized calls skip argument deserialization entirely
  • Synchronous fast-path -- OnAuthorize returning ValueTask<AuthorizeResult> with a synchronous result allocates nothing
  • Static permission arrays -- int[] arrays are emitted as static readonly class fields, allocated once
  • AOT compatible -- all permission values are resolved at compile time by the source generator; no reflection or runtime type inspection
  • Authorization caching -- opt-in TTL-based caching avoids repeated OnAuthorize calls for hot paths; thread-safe ConcurrentDictionary<int, ...> per session keyed by method ID with lock-free reads

Security Considerations

  • Fail-safe on exception: If OnAuthorize throws, the session is disconnected (logged as error). This prevents accidental authorization bypass from buggy auth logic
  • Server-side only: Authorization is enforced server-side; clients cannot bypass it. The attribute is a compile-time error on client nexuses (NEXNET024)
  • Per-invocation: Auth is checked on every invocation (unless cached), not permanently cached. After reconnection, auth is re-evaluated naturally. Collection reconnections go through InvokeMethodCore and are re-authorized
  • Compile-time errors: NEXNET025 errors if [NexusAuthorize] is used but OnAuthorize is not overridden (default allows all). Applies to both methods and collections
  • Enum type safety: NEXNET027 errors if the permission enum is not backed by int (the default), since permission values are stored as Int32
  • Pool-safe unauthorized response: InvocationResultMessage is disposed in a try/finally block to prevent pool leaks if SendMessage throws
  • Cache security: Only Allowed and Unauthorized are cached; Disconnect and exception paths are never cached. Cache is per-session with explicit invalidation support. CacheDurationSeconds = 0 provides an explicit no-cache override for sensitive methods
  • Collection auth note: AuthorizeResult.Unauthorized on a collection silently drops the request since collections lack a return channel. Use AuthorizeResult.Disconnect to actively reject unauthorized collection access

Diagnostics

ID Severity Description
NEXNET024 Error [NexusAuthorize] used on a client nexus (server-only)
NEXNET025 Error [NexusAuthorize] used but OnAuthorize not overridden
NEXNET026 Error Mixed permission enum types across [NexusAuthorize] attributes
NEXNET027 Error Permission enum not backed by int (the default)

Breaking changes

Public consumer-facing changes

None. All additions are backward-compatible:

  • AuthorizeResult, NexusAuthorizeAttribute<T>, ProxyUnauthorizedException are new types
  • OnAuthorize virtual has a default implementation returning Allowed
  • DisconnectReason.Unauthorized is a new enum member (underlying byte value 34)
  • ServerConfig.AuthorizationCacheDuration defaults to null (disabled)
  • NexusAuthorizeAttribute.CacheDurationSeconds defaults to -1 (use server config)

Internal non-consumer changes

  • InvocationResultMessage.StateType gains Unauthorized = 3 -- older clients receiving this from a newer server will hit the default switch case and throw InvalidOperationException. This only occurs if the feature is actively used against an older client.
  • ProxyRemoteInvocationException gains a protected constructor accepting a string message parameter (for ProxyUnauthorizedException inheritance). This is binary-compatible but a minor API surface addition.
  • MessageType.DisconnectUnauthorized = 34 -- older peers receiving this will not recognize the disconnect reason, but will still disconnect cleanly as the transport closes.

DJGosnell and others added 10 commits February 20, 2026 20:57
Introduces AuthorizeResult enum, NexusAuthorizeAttribute<TPermission>,
ProxyUnauthorizedException, OnAuthorize virtual on ServerNexusBase,
Unauthorized state on InvocationResultMessage, and Unauthorized
disconnect reason. This is the runtime foundation for generator-emitted
auth guards.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Generator extracts [NexusAuthorize<TPermission>] from class methods,
correlates with interface methods, emits static permission arrays and
auth guards in InvokeMethodCore switch cases. Adds NEXNET024-026
diagnostics for client usage, missing OnAuthorize override, and mixed
permission enum types.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Generator tests verify diagnostics (NEXNET024-026) and successful code
generation for authorized, marker-only, and multi-permission methods.
Integration tests cover Allowed/Unauthorized/Disconnect flows, parameter
verification, return values, method body non-execution, and exception
handling across all 6 transport types (66 tests).

Refactored ServerNexusBase to expose a single protected
CheckAuthorization method that handles the full auth lifecycle
(authorize, send unauthorized result, disconnect) internally,
keeping generated code minimal.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The pinned libmsquic 2.4.8 was removed from Microsoft's Ubuntu 24.04
apt repository. Install latest available version instead and run
apt-get update first to ensure fresh package index.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Using [NexusAuthorize] without overriding OnAuthorize now fails
compilation, ensuring fail-closed authorization by default.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extend [NexusAuthorize<TPermission>] to work on collection properties
alongside methods. The auth guard is emitted before StartCollection in
InvokeMethodCore, so every collection connect/reconnect is authorized.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Wrap InvocationResultMessage rent/send/dispose in try/finally in
  CheckAuthorization to prevent pool leak if SendMessage throws
- Add NEXNET027 compile error when permission enum underlying type is
  Int64/UInt64 (values stored as Int32 would silently overflow)
- Add integration test verifying multiple permissions on a single method
  are correctly passed through to OnAuthorize
- Fix comment typo in MessageType.cs ("Reserved through -39" → "39")

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add per-session caching of authorization results with configurable TTL
at two levels:
- ServerConfig.AuthorizationCacheDuration: server-wide default (null =
  disabled)
- NexusAuthorizeAttribute.CacheDurationSeconds: per-method/collection
  override (-1 = use server config, 0 = never cache, >0 = seconds)

Method attribute always wins when set. Only Allowed and Unauthorized
results are cached; Disconnect and exception paths are never cached.

Includes InvalidateAuthorizationCache() overloads for explicit cache
clearing (all entries or by method ID), and an internal TickCountOverride
for deterministic test timing without Task.Delay.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… perm fields by ID

- Replace Dictionary with ConcurrentDictionary for thread-safe
  concurrent access to the authorization cache
- Whitelist int-only for permission enum underlying type (NEXNET027)
  instead of blacklisting long/ulong, catching uint/byte/etc.
- Key generated __authPerms_ fields by method/collection ID instead
  of name to prevent collisions from method overloads
- Add concurrent cache access integration test using
  ManualResetEventSlim to force thread contention
- Add byte-backed enum generator test
- Fix test name AuthorizeWithoutOnAuthorize_EmitsWarning -> _EmitsError

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add comprehensive authorization documentation covering setup,
authorization results, caching configuration, and compile-time
diagnostics. Update llm-dev.md with new types, models, and test
files. Update llm-usage.md with usage examples and config table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@DJGosnell DJGosnell merged commit 620b16a into master Mar 16, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant