lilydocs
HTTP APIWebSocketConsumer
Connections4 min read

Authentication

Connect your real credential verifier to the handshake scope and give the connection a verified Principal.

Lily's authentication support is a single global identity middleware slot. It runs before 101, can resolve scoped services, and converts verification results into connection identity. JWT, an OIDC provider, or a session store is your application's integration.

Connect a real verifier#

The following example is for service/CLI clients that can send an Authorization header. SessionVerifier is an application-defined interface; declaring it alone does not configure authentication. Add its real implementation through DI registration and implement ServiceTrait. Interface registration uses #[service(interface = dyn SessionVerifier, lifetime = "Scoped")] or a suitable Singleton lifetime.

identity.rs
use std::sync::Arc;
use lilyrs::websocket::{AuthenticatedWebSocketIdentity, ExecutionCancellation, Extensions,
    HeaderValue, Principal, WsAppBuilder, async_trait};
use lilyrs::websocket::middleware::{MiddlewareDescriptor, MiddlewareErrorCode, MiddlewareKind,
    WebSocketIdentity, WebSocketIdentityMiddleware, WsHandshakeExchange,
    WsHandshakeRejection, WsMiddlewareInitError};

#[derive(Clone)]
pub struct TenantId(pub String);
pub struct VerifiedSession {
    pub principal: Principal,
    pub tenant: TenantId,
    pub expires_at: tokio::time::Instant,
}

// The application binds this interface to its real verifier through DI.
// This service owns signature, issuer/audience, expiry and session revocation checks.
#[async_trait]
pub trait SessionVerifier: Send + Sync {
    async fn verify(&self, authorization: &str) -> Option<VerifiedSession>;
}

pub struct SessionIdentity;
fn unauthorized() -> WsHandshakeRejection {
    WsHandshakeRejection::unauthorized(
        MiddlewareErrorCode::new("AUTH_REQUIRED").expect("sabit kod"),
        HeaderValue::from_static("Bearer"),
    ).expect("geçerli challenge")
}
fn unavailable() -> WsHandshakeRejection {
    WsHandshakeRejection::try_new(503,
        MiddlewareErrorCode::new("AUTH_UNAVAILABLE").expect("sabit kod"),
    ).expect("geçerli durum")
}

#[async_trait]
impl WebSocketIdentityMiddleware for SessionIdentity {
    async fn new(_: Arc<Extensions>) -> Result<Self, WsMiddlewareInitError> { Ok(Self) }
    fn descriptor(&self) -> MiddlewareDescriptor {
        MiddlewareDescriptor::new("session_identity", MiddlewareKind::WebSocketHandshake)
    }
    async fn identify(&self, exchange: &mut WsHandshakeExchange,
        cancellation: ExecutionCancellation) -> Result<WebSocketIdentity, WsHandshakeRejection>
    {
        let authorization = exchange.request().headers().get_custom_header("authorization")
            .ok_or_else(unauthorized)?.to_owned();
        let verifier = exchange.service::<dyn SessionVerifier>().await
            .map_err(|_| unavailable())?;
        let verified = tokio::select! {
            biased;
            _ = cancellation.cancelled() => return Err(unavailable()),
            result = verifier.verify(&authorization) => result.ok_or_else(unauthorized)?,
        };
        let authenticated = AuthenticatedWebSocketIdentity::try_new(verified.principal)
            .map_err(|_| unavailable())?.expires_at(verified.expires_at);
        let mut identity = WebSocketIdentity::authenticated(authenticated);
        identity.insert_connection_local(verified.tenant).map_err(|_| unavailable())?;
        Ok(identity)
    }
}
pub fn with_identity(builder: WsAppBuilder) -> WsAppBuilder {
    builder.identity_middleware::<SessionIdentity>()
}

Download the complete example.

Save it as src/identity.rs. Pass the builder through identity::with_identity(builder) or call .identity_middleware::<identity::SessionIdentity>(). Do not use both; there is one identity middleware per app.

Verification responsibilities#

The verifier enforces credential rules such as signature, issuer, audience, expiration, and session revocation. Create the Principal only after these checks. Parsing a header or reading a token's subject does not replace verification.

Missing/invalid credentials are rejected with 401, and an unavailable verifier with 503. The 401 carries a valid Bearer challenge. Internal provider error text is not copied to the public response. On cancellation, the example stops waiting for a new verification result; the real verifier must also define its own I/O ownership and cancellation contract.

What transfers to the connection?#

The verifier in the handshake scope does not transfer. The verified Principal, optional monotonic expiry, and owned TenantId do. Connection locals hold at most 32 typed values. PrincipalId is a nonempty exact identity of at most 256 UTF-8 bytes, with no surrounding whitespace or control characters.

If tenants share subject values, separate them through your namespace and recipient-key policy; Lily does not infer tenant semantics. Each message receives an immutable identity snapshot. Enforce role, scope, and object permissions through a guard.

Public namespaces and browsers#

When an app contains both public and private namespaces, identity middleware can branch on the exact namespace and return WebSocketIdentity::anonymous() for a public one. Required Principal extraction does not succeed on an anonymous connection; use Option<Principal> for deliberate optional access.

The browser constructor does not accept arbitrary Authorization headers. Follow the sessions/CSRF flow for browser login and cookie verification. Identity lifecycle explains refresh and expiry.

Sources for this page v0.1.0

This guide is based on the reviewed framework commit.

QUICK START
Documentation
Source code