Source: docs/cookbook-interceptors.md · Edit on GitHub
Cookbook: interceptors, auth, TLS / mTLS
Logging interceptor
let sink ← IO.mkRef (#[] : Array String)
let chain : Array Grpc.Interceptor.ServerUnary := #[Grpc.Interceptor.loggingServer sink]
let mut s := Grpc.Server.empty
s := Grpc.Interceptor.registerUnary s "demo.Svc" "Ping" chain fun req => do
pure (req, .ok)
Client side:
let res ← Grpc.Interceptor.callUnary ch "demo.Svc" "Ping"
#[Grpc.Interceptor.loggingClient sink] reqBytes
Bearer token (call credentials)
ClientUnary only wraps the request body, so attach auth metadata on the call (or via dial call-creds):
let md := Grpc.Interceptor.bearerMetadata "my-token"
let res ← Grpc.Channel.unary ch "svc" "Method" req md
-- Or dial-wide:
let ch ← Grpc.Channel.dial "127.0.0.1:50051" {
call := some (Grpc.Credentials.CallCredentials.accessToken "my-token")
}
Deadlines
-- 100 milliseconds
let res ← Grpc.Channel.unary ch "svc" "Slow" req {} (some "100m")
-- 2 seconds
let res ← Grpc.Channel.unary ch "svc" "Slow" req {} (some "2S")
On deadline expiry the client maps to DEADLINE_EXCEEDED (and may RST the stream).
TLS dial
let ch ← Grpc.Channel.dial "api.example.com:443" {
channel := .tls {
caPath := some "/etc/ssl/certs/ca-certificates.crt"
serverName := some "api.example.com"
}
}
mTLS → read ctx.peerIdentity in the handler
Client presents a certificate; server requires and verifies one, then exposes the verified peer identity to unary handlers via ServerCallContext:
-- Client
let ch ← Grpc.Channel.dial "localhost:50051" {
channel := .tls {
caPath := some "certs/ca.pem"
certPath := some "certs/client.pem"
keyPath := some "certs/client.key"
serverName := some "localhost"
}
}
-- Server
let mut s := Grpc.Server.empty
s := Grpc.Server.registerWithContext s "demo.Svc" "Ping" fun ctx req => do
match ctx.peerIdentity with
| none =>
-- Under mTLS (`clientCaPath` set) this should be unreachable after a
-- successful handshake; treat as miswire / UNAUTHENTICATED.
pure (ByteArray.empty, .unauthenticated "mtls_required")
| some id =>
-- Map id.uriSans / id.commonName → principal via local policy.
-- Subject DN is OpenSSL RFC 2253 form.
pure (req, .ok)
-- Optional interceptor that fails closed when mTLS was configured but identity is missing:
s := Grpc.Interceptor.registerUnaryWithContext s "demo.Svc" "Secure"
#[Grpc.Interceptor.requirePeerIdentity] fun ctx req => do
pure (req, .ok)
Grpc.Server.serveTls s {
certPath := some "certs/server.pem"
keyPath := some "certs/server.key"
clientCaPath := some "certs/ca.pem"
} { host := "127.0.0.1", port := 50051 }
Do not treat inbound metadata such as x-client-subject, x-forwarded-client-cert, or a self-declared principalId in the request body as AuthN. Those values are attacker-controlled unless you terminate TLS at a trusted proxy and document a separate trusted-proxy identity mode (not provided by lean-grpc today). Identity fields are only as trustworthy as the clientCaPath private CA / PKI.
End-to-end coverage: tlsLoopback (plain TLS, reject without client cert, accept with client cert, dual-cert identity binding, metadata non-forgery, h2c → none, accept-loop survival after failed handshake).
Working example: Examples/MirrorForge/ — Stamp uses registerUnaryWithContext (Bearer metadata preferred, body token fallback). Set TLS_CERT/TLS_KEY/ TLS_CLIENT_CA on the server and TLS_CA (+ client cert) on the client for mTLS; see Examples/MirrorForge/README.md.
Streaming handlers do not yet receive ServerCallContext (follow-on).
See also
- TLS / Envoy
Tests/OpsSmoke.lean— interceptor + health/reflection/channelz smokeTests/TlsLoopback.lean— mTLS + peer-identity loopback
Source: docs/cookbook-interceptors.md in RileyBetts/lean-grpc. Hosted docs may lag the repository slightly.