Skip to content

Authentication

samgerene edited this page Oct 3, 2026 · 1 revision

Authentication

ArgusTransfer authenticates requests with a pluggable IArgusAuthenticationHandler run by the built-in ArgusAuthenticationMiddleware. Clients identify themselves with the standard Authorization header; the middleware rejects requests to protected endpoints with 401 Unauthorized or 403 Forbidden problem details.

The Authorization header

Requests carry credentials in the Authorization header in the form {scheme} {parameter}:

Authorization: Bearer token123

ArgusRequest has convenience members over Headers["Authorization"]:

Member Description
Authorization The raw header value (get/set). Setting null or an empty string removes the header.
AuthorizationScheme The scheme parsed at the first space or tab, e.g. Bearer. null when the header is absent. Schemes are case-insensitive -- compare with StringComparison.OrdinalIgnoreCase.
AuthorizationParameter Everything after the scheme, trimmed, e.g. token123. Parameters containing : or spaces (Basic, Digest) stay intact. null when absent or scheme-only.
SetAuthorization(scheme, parameter) Sets the header to {scheme} {parameter}, or {scheme} when the parameter is empty. The scheme must be a single token.

Values containing a carriage return or line feed are rejected with ArgumentException, so a value cannot inject additional headers on the wire. The header carries credentials -- never log it.

On the client:

var request = new ArgusRequest { Verb = ArgusVerb.GET, Route = "/items" };
request.SetAuthorization("Bearer", token);

var response = await client.SendAsync(request);

Writing a handler

public class ApiKeyHandler : IArgusAuthenticationHandler
{
    public Task<ArgusAuthenticationResult> AuthenticateAsync(ArgusContext context)
    {
        var request = context.Request;

        if (request.AuthorizationScheme == null)
        {
            return Task.FromResult(ArgusAuthenticationResult.NoResult());
        }

        if (!string.Equals(request.AuthorizationScheme, "ApiKey", StringComparison.OrdinalIgnoreCase)
            || !IsValidKey(request.AuthorizationParameter))
        {
            return Task.FromResult(ArgusAuthenticationResult.Fail("Invalid API key"));
        }

        var identity = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, "reporting-service") }, "ApiKey");
        return Task.FromResult(ArgusAuthenticationResult.Success(new ClaimsPrincipal(identity)));
    }
}

Handlers are registered as singletons and must be thread-safe. The handler receives the full ArgusContext, including EndpointMetadata (for per-endpoint decisions) and RequestAborted.

Results

Result Meaning Response on a protected endpoint
ArgusAuthenticationResult.Success(principal) Authenticated; principal becomes context.User request continues
ArgusAuthenticationResult.NoResult() No credentials the handler recognizes 401 Unauthorized
ArgusAuthenticationResult.Fail(reason) Invalid credentials 401 Unauthorized
ArgusAuthenticationResult.Forbidden(reason) Valid credentials, but the caller may not access the endpoint 403 Forbidden

A handler returning null is treated as NoResult(). The optional reason becomes the problem details detail; without one a generic message is used (ArgusAuthenticationMiddleware.UnauthorizedDetail / ForbiddenDetail). Don't put secrets in reasons. The instance is the request correlation token, and each rejection is logged as a warning with verb, route, correlation token and status -- never the credentials.

Registration

services.AddArgusModules();
services.AddArgusAuthentication<ApiKeyHandler>();

AddArgusAuthentication<THandler>(configure) registers the handler and the middleware as singletons. When called more than once, the first registered handler is used. The router created by AddArgusModules() adds the middleware as a global middleware directly after the exception handler and before any middleware your modules add, regardless of the order of the calls:

ArgusExceptionHandlerMiddleware   (AddArgusExceptionHandler, optional)
ArgusAuthenticationMiddleware     (AddArgusAuthentication)
module global middleware          (app.UseMiddleware in AddRoutes)
per-endpoint middleware           (.WithMiddleware)
endpoint handler

So unauthenticated requests never reach module middleware, and an exception thrown by a handler becomes a 500 from the exception handler.

Protecting endpoints

By default every endpoint requires an authenticated caller (ArgusAuthenticationOptions.RequireAuthentication = true). Override it per endpoint:

app.MapGet("/health", HealthHandler).AllowAnonymous();          // open
app.MapGet("/items", ItemsHandler);                               // protected (default)

Or make endpoints open by default and protect selected ones:

services.AddArgusAuthentication<ApiKeyHandler>(options => options.RequireAuthentication = false);

app.MapDelete("/items/{id:Guid}", DeleteHandler).RequireAuthentication();

.RequireAuthentication() and .AllowAnonymous() set the endpoint metadata authorize (ArgusAuthenticationOptions.AuthorizeMetadataKey) to "true" / "false"; you can also call .WithMetadata("authorize", "true") directly. A value other than true/false requires authentication, so a typo never opens an endpoint.

The handler also runs for anonymous endpoints, so context.User is set when a caller sends valid credentials -- useful for endpoints that behave differently for known callers.

Using the caller in handlers

app.MapGet("/items", context =>
{
    var caller = context.User.Identity.Name;           // ClaimsPrincipal from the handler
    var isAdmin = context.User.IsInRole("admin");
    // ...
    return Task.CompletedTask;
});

context.User is null for anonymous requests or when no authentication is registered.

Security notes

  • The Authorization header is supplied by the client: its value is only as trustworthy as the credential it carries. Use secrets (tokens, keys) that other local processes cannot guess, and protect them at rest.
  • Restrict who can connect to the pipe at all with Server Configuration → Pipe Security.
  • Routes that do not match return 404/501 before the pipeline runs, so authentication does not hide which routes exist.

See also

Clone this wiki locally