# Configuring endpoint connection hooks for authentication in iroh

> Secure iroh connections with custom authentication using endpoint connection hooks. Intercept QUIC handshakes for token-based pre-authorization. Learn more.

- Repository: [number zero/iroh](https://github.com/n0-computer/iroh)
- Tags: how-to-guide
- Published: 2026-07-02

---

**Use the `EndpointHooks` trait in [`iroh/src/endpoint/hooks.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/hooks.rs) to intercept connections before and after the QUIC handshake, enabling custom authentication flows like token-based pre-authorization without modifying underlying protocols.**

The iroh networking library from n0-computer provides a flexible hook system that lets you inject custom logic into the connection lifecycle. By implementing the `EndpointHooks` trait, you can configure endpoint connection hooks for authentication in iroh to validate remote identities, enforce rate limits, or implement token-based authorization before application data flows.

## The EndpointHooks trait architecture

The hook system is defined in [`iroh/src/endpoint/hooks.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/hooks.rs) and centers on a single trait that enables asynchronous interception of connection events.

```rust
pub trait EndpointHooks: Send + Sync + 'static {
    async fn before_connect<'a>(
        &'a self,
        remote_addr: &'a EndpointAddr,
        alpn: &'a [u8],
    ) -> BeforeConnectOutcome;

    async fn after_handshake<'a>(
        &'a self,
        conn: &'a Connection,
    ) -> AfterHandshakeOutcome;
}

```

- **`before_connect`** runs *before* the QUIC handshake completes. It receives the remote address and ALPN protocol identifier, returning either `Accept` to proceed or `Reject` to abort the connection attempt.
- **`after_handshake`** runs *after* the handshake succeeds, when the remote's identity is verified and the ALPN is negotiated. This is the appropriate place to check authentication state against the specific connection.

Hooks are stored in an `EndpointHooksList` inside the endpoint builder and execute in the order they were added via `Builder::hooks`.

## Implementing outgoing authentication hooks

For clients that must authenticate before connecting to protected services, implement an outgoing hook that performs pre-flight token validation.

**File:** [`iroh/examples/auth-hook.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/auth-hook.rs) (module `auth::OutgoingAuthHook`)

The outgoing hook uses a channel-based architecture to communicate with a background task:

```rust
pub struct OutgoingAuthHook {
    tx: mpsc::Sender<(EndpointId, oneshot::Sender<AuthResult>)>,
}

```

In `before_connect`, the hook checks if the connection is an auth request itself (avoiding recursive authentication). For normal connections, it calls `authenticate(remote_id).await`:

```rust
async fn before_connect<'a>(
    &'a self,
    remote_addr: &'a EndpointAddr,
    alpn: &'a [u8],
) -> BeforeConnectOutcome {
    // Skip auth protocol connections to avoid recursion
    if alpn == AUTH_ALPN {
        return BeforeConnectOutcome::Accept;
    }
    
    // Attempt authentication via background task
    match self.authenticate(remote_addr.endpoint_id).await {
        Ok(()) => BeforeConnectOutcome::Accept,
        Err(_) => BeforeConnectOutcome::Reject,
    }
}

```

The background `OutgoingAuthTask` manages a set of allowed remote IDs and pending requests. When authentication is required, it spawns a dedicated connection using the auth ALPN (`b"iroh-example/auth/0"`), sends the secret token over a unidirectional stream, and interprets the close code: `CLOSE_ACCEPTED` (1) indicates success, while `CLOSE_DENIED` (403) indicates failure.

## Implementing incoming authentication hooks

For servers that must verify clients have pre-authenticated, implement the incoming hook to gate access based on a shared allow-list.

**File:** [`iroh/examples/auth-hook.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/auth-hook.rs) (module `auth::IncomingAuthHook`)

The `IncomingAuthHook` implements `after_handshake` to check if the remote ID exists in the `allowed_remotes` set:

```rust
async fn after_handshake<'a>(
    &'a self,
    conn: &'a Connection,
) -> AfterHandshakeOutcome {
    let remote_id = conn.endpoint_id();
    
    // Allow the auth protocol itself
    if conn.alpn() == Some(AUTH_ALPN) {
        return AfterHandshakeOutcome::Accept;
    }
    
    // Check if pre-authenticated
    if self.allowed_remotes.lock().unwrap().contains(&remote_id) {
        AfterHandshakeOutcome::Accept
    } else {
        AfterHandshakeOutcome::Close(403u32.into(), "not authenticated")
    }
}

```

The `AuthProtocol` handler runs on the server side to process authentication requests. It reads the token from the connection, validates it against the expected secret, and on success inserts the remote ID into the shared `allowed_remotes` set:

```rust
async fn accept(&self, conn: quinn::Incoming) -> anyhow::Result<()> {
    let conn = conn.await?;
    let (mut send, mut recv) = conn.accept_uni().await?;
    let token = recv.read_to_end(1024).await?;
    
    if token == self.expected_token {
        self.allowed_remotes.lock().unwrap().insert(conn.endpoint_id());
        conn.close(1u32.into(), b"accepted"); // CLOSE_ACCEPTED
    } else {
        conn.close(403u32.into(), b"denied"); // CLOSE_DENIED
    }
    Ok(())
}

```

## Wiring hooks to your endpoint

To configure endpoint connection hooks for authentication in iroh, install the hooks during endpoint construction and wire the auth protocol into a router.

**Server-side setup:**

```rust
use iroh::{Endpoint, protocol::Router, endpoint::presets};

let secret_token = b"super-secret".to_vec();
let (auth_hook, auth_protocol) = auth::incoming(secret_token);

let endpoint = Endpoint::builder(presets::N0)
    .hooks(auth_hook)  // Install the incoming hook
    .bind()
    .await?;

let router = Router::builder(endpoint)
    .accept(auth::ALPN, auth_protocol)  // Register auth protocol
    .accept(b"my-app/0", MyAppHandler)
    .spawn();

```

**Client-side setup:**

```rust
let token = b"super-secret".to_vec();
let (auth_hook, auth_task) = auth::outgoing(token);

let endpoint = Endpoint::builder(presets::N0)
    .hooks(auth_hook)  // Install the outgoing hook
    .bind()
    .await?;

// Spawn the background authentication task
let _guard = auth_task.spawn(endpoint.clone());

// Subsequent connections automatically authenticate
let conn = endpoint.connect(remote_addr, b"my-app/0").await?;

```

## Running the complete example

The repository includes a fully functional demonstration that exercises both sides of the authentication flow.

```bash
cargo run --example auth-hook

```

The example executes three scenarios sequentially:
1. Connection without authentication — rejected with "not authenticated"
2. Connection with invalid token — rejected with "authentication denied"
3. Connection with valid token — succeeds and exchanges data

Review the complete implementation in [`iroh/examples/auth-hook.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/auth-hook.rs) to see how the `OutgoingAuthTask` coordinates asynchronous token exchange while the `IncomingAuthHook` maintains the allow-list state.

## Summary

- **Endpoint hooks** in [`iroh/src/endpoint/hooks.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/hooks.rs) provide `before_connect` for pre-handshake filtering and `after_handshake` for post-handshake authorization.
- **Outgoing hooks** pair with background tasks to perform asynchronous authentication via dedicated protocol connections before allowing application traffic.
- **Incoming hooks** check shared state (such as `allowed_remotes`) populated by a protocol handler that validates tokens and tracks authenticated peers.
- Hooks are installed via `Builder::hooks` and execute sequentially; multiple hooks can be stacked for layered policies.
- The auth-hook example demonstrates token-based pre-authentication using close codes (`CLOSE_ACCEPTED` vs `CLOSE_DENIED`) to signal success or failure.

## Frequently Asked Questions

### How do I prevent authentication from blocking the endpoint builder?

The outgoing hook uses a non-blocking channel (`mpsc::Sender`) to communicate with a background task (`OutgoingAuthTask`), allowing the `before_connect` hook to await authentication without blocking the endpoint builder or other connections. The hook itself remains lightweight while the background task handles the actual network I/O for token exchange.

### Can I use multiple endpoint hooks simultaneously?

Yes. The `Endpoint` builder stores hooks in an `EndpointHooksList` and invokes them in registration order. If any hook returns `Reject` or `Close`, the connection attempt aborts immediately. This allows you to combine authentication hooks with rate-limiting or logging hooks by simply calling `.hooks(hook1).hooks(hook2)` during endpoint construction.

### What is the difference between rejecting in `before_connect` versus `after_handshake`?

`before_connect` runs before the QUIC handshake completes, allowing you to reject connections based on IP address or ALPN without incurring the cost of cryptographic verification. `after_handshake` runs after the handshake succeeds, providing access to the authenticated remote identity (`EndpointId`), making it the correct place to verify pre-authentication status or implement identity-based access control.

### How does the auth protocol avoid infinite recursion?

The outgoing hook checks `if alpn == AUTH_ALPN` and returns `BeforeConnectOutcome::Accept` immediately for auth protocol connections. This prevents the hook from attempting to authenticate the authentication connection itself, which would create a recursive loop. The incoming hook similarly exempts the auth ALPN from its allow-list check.