Authentication Middleware in Hyperlane
Project Code:https://github.com/hyperlane-dev/hyperlane
Introduction
Authentication is a critical security concern for web applications. Whether you are building a REST API, a web application, or a microservice, controlling who can access your resources is paramount. In the hyperlane framework, authentication is typically implemented as middleware — reusable components that intercept requests before they reach your route handlers.
This article explores various authentication patterns you can implement in hyperlane, from simple header-based checks to more sophisticated token validation schemes.
Understanding Middleware in Hyperlane
Before diving into authentication, it helps to understand how middleware works in hyperlane. Middleware components implement the ServerHook trait, which provides two key methods:
-
new()— Called when a new connection is established. Used for initialization. -
handle()— Called for each request on the connection. This is where authentication logic lives.
The handle() method returns a Status enum value:
-
Status::Continue— Allow the request to proceed to the next handler. -
Status::Reject— Reject the request and stop further processing.
Basic Authentication Middleware
Header-Based Authentication
The simplest form of authentication checks for the presence of an Authorization header. Here is the foundational authentication middleware from hyperlane's documentation:
impl ServerHook for AuthMiddleware {
async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
let auth_str = ctx.get_request()
.try_get_header_back(AUTHORIZATION)
.unwrap_or_default();
if auth_str.is_empty() {
let data = ctx.get_mut_response()
.set_status_code(401)
.set_body("Unauthorized")
.build();
if stream.try_send(data).await.is_err() {
stream.set_closed(true);
}
return Status::Reject;
}
Status::Continue
}
}
This middleware:
- Reads the
Authorizationheader from the incoming request. - If the header is missing or empty, it responds with a 401 Unauthorized status and rejects the request.
- If the header exists, it allows the request to continue.
Using try_get_header_back
The try_get_header_back method is used here because the Authorization header is typically added by the client (the "back" direction from the server's perspective). This method returns an Option<String>, and unwrap_or_default() converts it to an empty string if the header is absent.
Error Handling in Authentication
Notice the careful error handling when sending the rejection response:
if stream.try_send(data).await.is_err() {
stream.set_closed(true);
}
If the send fails (e.g., because the client has already disconnected), the stream is marked as closed to prevent further attempts to write to it.
Bearer Token Authentication
Validating Bearer Tokens
A common authentication pattern is Bearer token authentication, where the Authorization header contains a token prefixed with "Bearer":
struct BearerAuthMiddleware;
impl ServerHook for BearerAuthMiddleware {
async fn new(_: &mut Stream, _: &mut Context) -> Self {
Self
}
async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
let auth_str = ctx.get_request()
.try_get_header_back(AUTHORIZATION)
.unwrap_or_default();
if !auth_str.starts_with("Bearer ") {
let data = ctx.get_mut_response()
.set_status_code(401)
.set_body("Unauthorized: Invalid token format")
.build();
if stream.try_send(data).await.is_err() {
stream.set_closed(true);
}
return Status::Reject;
}
let token = &auth_str[7..]; // Extract token after "Bearer "
if token.is_empty() {
let data = ctx.get_mut_response()
.set_status_code(401)
.set_body("Unauthorized: Empty token")
.build();
if stream.try_send(data).await.is_err() {
stream.set_closed(true);
}
return Status::Reject;
}
// Store the token in context attributes for downstream handlers
ctx.set_attribute("auth_token", token);
Status::Continue
}
}
server.request_middleware::<BearerAuthMiddleware>();
This enhanced middleware:
- Checks that the
Authorizationheader starts with"Bearer ". - Extracts the actual token value.
- Stores the token in the context attributes so downstream handlers can access it.
- Returns appropriate error messages for different failure scenarios.
Method-Based Authentication
Sometimes you only want to allow specific HTTP methods. Hyperlane's attribute macros make this straightforward:
#[methods("GET", "POST")]
struct MethodRestrictedRoute;
impl ServerHook for MethodRestrictedRoute {
async fn new(_: &mut Stream, _: &mut Context) -> Self {
Self
}
async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
// Only GET and POST requests reach here
// Process the request...
Status::Continue
}
}
The #[methods] attribute automatically rejects requests that don't match the specified HTTP methods, before the handle() method is even called.
Route-Level Authentication
Using Route Filters
Hyperlane's routing system supports attribute-based filtering, which can be used for authentication and access control:
#[host("api.example.com")]
struct ApiRoute;
#[reject_host("blocked.example.com")]
struct SafeRoute;
#[filter(ctx.get_request().get_method() == &RequestMethod::Get)]
struct GetOnlyRoute;
#[reject(ctx.get_request().get_path().len() > 1000)]
struct SafePathRoute;
These filters provide different levels of access control:
-
#[host]— Only allow requests to a specific host. -
#[reject_host]— Block requests from specific hosts. -
#[filter]— Apply a custom filter expression. -
#[reject]— Reject requests matching a condition.
Referer-Based Filtering
You can also filter based on the Referer header:
#[referer("https://example.com")]
#[reject_referer("https://malicious.com")]
struct RefererFilteredRoute;
This is useful for preventing hotlinking or blocking requests from known malicious domains.
Combining Authentication with Other Middleware
Authentication and CORS
When building APIs that serve cross-origin requests, authentication middleware must work alongside CORS middleware. The CORS headers should be set regardless of whether authentication succeeds:
struct CorsAuthMiddleware;
impl ServerHook for CorsAuthMiddleware {
async fn new(_: &mut Stream, _: &mut Context) -> Self {
Self
}
async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
// Always set CORS headers first
ctx.get_mut_response()
.set_header(ACCESS_CONTROL_ALLOW_ORIGIN, WILDCARD_ANY)
.set_header(ACCESS_CONTROL_ALLOW_METHODS, ALL_METHODS)
.set_header(ACCESS_CONTROL_ALLOW_HEADERS, WILDCARD_ANY);
// Then perform authentication
let auth_str = ctx.get_request()
.try_get_header_back(AUTHORIZATION)
.unwrap_or_default();
if auth_str.is_empty() {
let data = ctx.get_mut_response()
.set_status_code(401)
.set_body("Unauthorized")
.build();
if stream.try_send(data).await.is_err() {
stream.set_closed(true);
}
return Status::Reject;
}
Status::Continue
}
}
Multiple Middleware with Priority
Hyperlane allows you to register multiple middleware with different priorities using the attribute macro system:
#[request_middleware(1)]
struct RequestMiddleware1;
#[request_middleware(2)]
struct RequestMiddleware2;
Lower numbers execute first. This lets you control the order in which authentication and other middleware run.
Authentication Using Context Attributes
Hyperlane's context attribute system allows authentication data to flow between middleware and route handlers:
impl ServerHook for AuthMiddleware {
async fn new(_: &mut Stream, ctx: &mut Context) -> Self {
Self
}
async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
let auth_str = ctx.get_request()
.try_get_header_back(AUTHORIZATION)
.unwrap_or_default();
if auth_str.is_empty() {
let data = ctx.get_mut_response()
.set_status_code(401)
.set_body("Unauthorized")
.build();
if stream.try_send(data).await.is_err() {
stream.set_closed(true);
}
return Status::Reject;
}
ctx.set_attribute("authenticated", "true");
ctx.set_attribute("auth_token", &auth_str);
Status::Continue
}
}
Then in your route handler:
impl ServerHook for ProtectedRoute {
async fn new(_: &mut Stream, ctx: &mut Context) -> Self {
Self
}
async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
let is_authenticated: Option<String> = ctx.try_get_attribute("authenticated");
if is_authenticated.is_some() {
// User is authenticated — serve the protected resource
ctx.get_mut_response()
.set_status_code(200)
.set_body("Protected content");
} else {
ctx.get_mut_response()
.set_status_code(403)
.set_body("Forbidden");
}
let data = ctx.get_mut_response().build();
stream.try_send(data).await;
Status::Continue
}
}
Registering Authentication Middleware
Authentication middleware is registered with the server using the request_middleware method:
server.request_middleware::<AuthMiddleware>();
For attribute macro-based middleware:
#[request_middleware(1)]
struct AuthMiddleware;
The middleware runs for every request that reaches it, making it an ideal place to enforce authentication policies.
Best Practices for Authentication Middleware
Fail fast. Reject unauthenticated requests as early as possible in the middleware chain to avoid unnecessary processing.
Provide clear error messages. Tell the client why their request was rejected (e.g., "Missing token", "Invalid token format", "Token expired").
Use context attributes for token propagation. Store validated token data in context attributes so downstream handlers don't need to re-parse the
Authorizationheader.Set CORS headers before authentication. For APIs, ensure CORS headers are present even on error responses so the client's browser can read the error.
Handle stream errors gracefully. Always check the result of
try_sendand close the stream if it fails.Layer your middleware. Use priority numbers to control the order of middleware execution. CORS middleware should typically run before authentication middleware.
Don't store sensitive data in cookies for API authentication. Use the
Authorizationheader for APIs and reserve cookies for browser-based sessions.
Conclusion
Hyperlane provides a flexible and powerful middleware system for implementing authentication in your web applications. From simple header checks to complex token validation, the ServerHook trait gives you full control over the authentication process. By combining authentication middleware with context attributes, route filters, and other middleware components, you can build secure and maintainable access control systems.
The key is to leverage hyperlane's middleware pipeline — registering authentication middleware with appropriate priority levels, using context attributes to propagate authentication state, and handling errors gracefully throughout the process.
Project Code:https://github.com/hyperlane-dev/hyperlane
Top comments (0)