Skip to content

Middleware

Middleware

How to use Middleware

Middleware is a way to intercept requests to perform some operations on them, like authentication, authorization, logging, etc and also act on the response headers.

Middleware can either be attached to individual handlers, attached at the server level (thus applied to all handlers), or both. They will be executed in the order they are attached, and they can stop the request processing by sending a response themselves.

You can have a look at the examples for some use cases.

For example, such middleware would handle authentication and set some attributes on the request to make them available for the next middleware and for the handler which will process the request.

AsyncMiddlewareFunction complexAuth([](AsyncWebServerRequest* request, ArMiddlewareNext next) {
  if (!request->authenticate("user", "password")) {
    return request->requestAuthentication();
  }

  request->setAttribute("user", "Mathieu");
  request->setAttribute("role", "staff");

  next(); // continue processing

  // you can act one the response object
  request->getResponse()->addHeader("X-Rate-Limit", "200");
});

Here are the list of available middlewares:

  • AsyncMiddlewareFunction: can convert a lambda function (ArMiddlewareCallback) to a middleware
  • AsyncAuthenticationMiddleware: to handle basic/digest authentication globally or per handler
  • AsyncAuthorizationMiddleware: to handle authorization globally or per handler
  • AsyncCorsMiddleware: to handle CORS preflight request globally or per handler
  • AsyncHeaderFilterMiddleware: to filter out headers from the request
  • AsyncHeaderFreeMiddleware: to only keep some headers from the request, and remove the others
  • LoggerMiddleware: to log requests globally or per handler with the same pattern as curl. Will also record request processing time
  • AsyncRateLimitMiddleware: to limit the number of requests on a windows of time globally or per handler

See the Middleware example here.

CORS with AsyncCorsMiddleware

You can handle CORS preflight requests and headers using AsyncCorsMiddleware.

static AsyncCorsMiddleware cors;

void setup() {
  // Configure CORS
  cors.setOrigin("http://my-client-app.com");
  cors.setMethods("POST, GET, OPTIONS, DELETE");
  cors.setHeaders("X-Custom-Header");
  cors.setAllowCredentials(true);
  cors.setMaxAge(600);

  // Add middleware to server (valid for all handlers)
  server.addMiddleware(&cors);

  // Or add to specific handler only
  // server.on("/api/data", HTTP_GET, handleData).addMiddleware(&cors);
}

Rate Limiting with AsyncRateLimitMiddleware

You can limit the number of requests within a time window using AsyncRateLimitMiddleware.

static AsyncRateLimitMiddleware rateLimit;

void setup() {
  // Allow maximum 5 requests per 10 seconds
  rateLimit.setMaxRequests(5);
  rateLimit.setWindowSize(10);

  // Apply globally
  // server.addMiddleware(&rateLimit);

  // OR apply to specific sensitive endpoint
  server.on("/login", HTTP_POST, handleLogin).addMiddleware(&rateLimit);
}

Authentication with AsyncAuthenticationMiddleware

IMPORTANT: Do not use the setUsername() and setPassword() methods on the handlers anymore. They are deprecated. These methods were causing a copy of the username and password for each handler, which is not efficient.

Now, you can use the AsyncAuthenticationMiddleware to handle authentication globally or per handler.

AsyncAuthenticationMiddleware authMiddleware;

// [...]

authMiddleware.setAuthType(AsyncAuthType::AUTH_DIGEST);
authMiddleware.setRealm("My app name");
authMiddleware.setUsername("admin");
authMiddleware.setPassword("admin");
authMiddleware.setAuthFailureMessage("Authentication failed");
authMiddleware.generateHash(); // optimization to avoid generating the hash at each request

// [...]

server.addMiddleware(&authMiddleware); // globally add authentication to the server

// [...]

myHandler.addMiddleware(&authMiddleware); // add authentication to a specific handler

See the Auth example here.

Digest authentication: replay protection

Digest authentication includes server-side protections against replay attacks, using a stateless scheme (no per-nonce storage, keeping the server's memory footprint constant):

  • Signed nonce: each challenge nonce embeds its issuing time and is signed with a per-server-instance secret (HMAC-MD5). A nonce not issued by this server is rejected, and an expired nonce triggers a new challenge with stale=true (RFC 7616 §3.3), so compliant clients (browsers, curl) re-authenticate silently without prompting the user again.
  • Opaque: each challenge carries an opaque derived from the instance secret, identifying the server instance that issued the nonce.

The nonce validity window defaults to 60 seconds (ASYNCWEBSERVER_DIGEST_NONCE_TTL build flag) and can be tuned at runtime:

server.setDigestNonceTimeout(300); // extend nonce lifetime to 5 minutes

Changing the instance secret instantly revokes every nonce in circulation (all clients get re-challenged):

uint8_t secret[16] = {/* 16 random bytes */};
server.setDigestSecret(secret); // kill-switch: invalidates all outstanding nonces

[!NOTE] As with any stateless scheme, a captured Authorization: Digest ... header remains valid for the remainder of the nonce TTL. The library does not keep the raw request-target in memory, so the header uri field is not checked by default — to bind a header to the resource it was issued for (RFC 7616 §3.4.6), pass the expected request-target with authenticate(..., nonce, opaque, uri) (see below). For one-shot semantics, keep the TTL short and serve Digest-protected endpoints over HTTPS.

For custom validation of every digest parameter, AsyncWebServerRequest::authenticate() accepts optional nonce, opaque and uri arguments — these only apply to the Digest auth scheme and are ignored otherwise. A NULL argument lets the library validate it automatically, while a non-NULL value is compared verbatim against the caller-supplied value:

// the library validates nonce and opaque (header uri is left unchecked)
request->authenticate("admin", "admin", "MyApp");

// the caller takes over the uri check and binds the header to a request-target
// (nonce/opaque are still validated by the library)
request->authenticate("admin", "admin", "MyApp", false, NULL, NULL, "/my/custom/uri");

Re-authentication required

AsyncWebServerRequest::reAuthRequired() returns true when the last authenticate() call failed for a reason that a re-challenge can resolve, i.e. re-sending the same credentials against a fresh challenge will eventually succeed.

With Digest auth: the credentials were correct and the nonce was issued by this server, but it expired. requestAuthentication() then automatically issues the next challenge with stale=true (RFC 7616 §3.3), so compliant clients (browsers, curl) re-authenticate silently without prompting the user again — no special handling is needed from your code:

if (!request->authenticate("admin", "admin", "MyApp")) {
  // reAuthRequired() is true when only the nonce expired: the challenge issued
  // below will carry stale=true and the client will retry silently
  request->requestAuthentication(AsyncAuthType::AUTH_DIGEST, "MyApp");
  return;
}

Future authentication mechanisms may reuse the same flag for their own equivalent conditions.

Migration to Middleware to improve performance and memory usage

  • AsyncEventSource.authorizeConnect(...) => do not use this method anymore: add a common AsyncAuthorizationMiddleware to the handler or server, and make sure to add it AFTER the AsyncAuthenticationMiddleware if you use authentication.
  • AsyncWebHandler.setAuthentication(...) => do not use this method anymore: add a common AsyncAuthenticationMiddleware to the handler or server
  • ArUploadHandlerFunction and ArBodyHandlerFunction => these callbacks receiving body data and upload and not calling anymore the authentication code for performance reasons. These callbacks can be called multiple times during request parsing, so this is up to the user to now call the AsyncAuthenticationMiddleware.allowed(request) if needed and ideally when the method is called for the first time. These callbacks are also not triggering the whole middleware chain since they are not part of the request processing workflow (they are not the final handler).