8.7 KiB
Authentication and Security
This document covers the authentication mechanisms, TLS configuration, and security-related features of the async-nats client.
Authentication Methods
The NATS server supports multiple authentication methods. The client implements all of them.
1. Username/Password
The simplest authentication method.
// Via ConnectOptions
let client = ConnectOptions::with_user_and_password("user".into(), "pass".into())
.connect("nats://localhost")
.await?;
// Via URL
let client = connect("nats://user:pass@localhost:4222").await?;
These credentials are sent in the CONNECT message as user and pass fields.
2. Token Authentication
A single token used for authentication.
let client = ConnectOptions::with_token("my-token".into())
.connect("nats://localhost")
.await?;
Token is sent in the CONNECT message as auth_token field.
3. NKey Authentication
NKey-based authentication using Ed25519 key pairs. Requires the nkeys feature.
let seed = "SUANQDPB2RUOE4ETUA26CNX7FUKE5ZZKFCQIIW63OX225F2CO7UEXTM7ZY";
let client = ConnectOptions::with_nkey(seed.into())
.connect("nats://localhost")
.await?;
Flow:
- Server sends
INFOwith anoncefield - Client creates a
KeyPairfrom the seed - Client signs the nonce:
key_pair.sign(nonce.as_bytes()) - Client sends
CONNECTwithnkey(public key) andsig(Base64URL-encoded signature) - Server verifies the signature against the public key and nonce
4. JWT Authentication
User JWT with a signing callback. Requires the nkeys feature.
let key_pair = Arc::new(nkeys::KeyPair::from_seed(seed)?);
let jwt = load_jwt().await?;
let client = ConnectOptions::with_jwt(jwt, move |nonce| {
let key_pair = key_pair.clone();
async move { key_pair.sign(&nonce).map_err(AuthError::new) }
})
.connect("nats://localhost")
.await?;
Flow:
- Server sends
INFOwith anoncefield - Client sends
CONNECTwithjwt(user JWT) andsig(Base64URL-encoded nonce signature) - The signing callback is async, allowing integration with external signing services (e.g., HSM)
5. Credentials File
Combines JWT and NKey from a .creds file. Requires the nkeys feature.
// From file
let client = ConnectOptions::with_credentials_file("path/to/my.creds")
.await?
.connect("nats://localhost")
.await?;
// From string
let client = ConnectOptions::with_credentials(creds_string)
.connect("nats://localhost")
.await?;
Credentials file format:
-----BEGIN NATS USER JWT-----
eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5...
------END NATS USER JWT------
************************* IMPORTANT *************************
NKEY Seed printed below can be used sign and prove identity.
-----BEGIN USER NKEY SEED-----
SUAIO3FHUX5PNV2LQIIP7TZ3N4L7TX3W53MQGEIVYFIGA635OZCKEYHFLM
------END USER NKEY SEED------
Location: auth_utils.rs handles parsing:
load_creds(path)— async file read + parseparse_jwt_and_key_from_creds(creds)— extracts JWT and KeyPair from the string
6. Auth Callback
A custom async callback that receives the server nonce and returns an Auth struct. This is the most flexible mechanism.
let client = ConnectOptions::with_auth_callback(move |nonce| {
async move {
let mut auth = Auth::new();
auth.username = Some("user".to_string());
auth.password = Some("pass".to_string());
// Can also set jwt, nkey, signature, token
Ok(auth)
}
})
.connect("nats://localhost")
.await?;
The callback is invoked on each connection/reconnection, allowing dynamic credential refresh (e.g., refreshing JWTs from an auth server).
7. URL-Embedded Credentials
// Username and password in URL
let client = connect("nats://user:pass@localhost:4222").await?;
// Token in URL (username field)
let client = connect("nats://token@localhost:4222").await?;
Auth Struct
Location: auth.rs
The Auth struct is a container for all authentication methods. Multiple fields can be set simultaneously:
#[derive(Clone, Default)]
pub struct Auth {
pub jwt: Option<String>,
pub nkey: Option<String>,
pub signature_callback: Option<CallbackArg1<String, Result<String, AuthError>>>,
pub signature: Option<Vec<u8>>,
pub username: Option<String>,
pub password: Option<String>,
pub token: Option<String>,
}
Priority in Connector::try_connect_to():
- Auth callback overrides all other methods
- NKey authentication (if
auth.nkeyis set) - JWT authentication (if
auth.jwtis set) - Username/password/token from
Authstruct - Username/password from URL
TLS Configuration
TLS Modes
| Mode | When | Description |
|---|---|---|
| None | Default | Plaintext connection |
| Standard | tls_required or server requires |
TLS after INFO |
| TLS First | tls_first option |
TLS before INFO |
| WebSocket | wss:// URL |
TLS handled by WebSocket library |
TLS Setup
Location: tls.rs
The config_tls() function builds a rustls::ClientConfig:
- Create
RootCertStoreand load native system certificates - Add custom root certificates from configured PEM files
- Build
ClientConfigwith the chosen crypto provider:ring(default)aws-lc-rsfips(aws-lc-rs in FIPS mode)
- If client certificate + key are configured, add them for mTLS
- If a custom
rustls::ClientConfigwas provided, use it directly
TLS First
let client = ConnectOptions::new()
.tls_first()
.connect("nats://localhost")
.await?;
This sets both tls_first = true and tls_required = true. The client performs TLS handshake before reading the INFO message. The server must have handshake_first: true in its configuration.
Custom TLS Configuration
let tls_client = rustls::ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth();
let client = ConnectOptions::new()
.require_tls(true)
.tls_client_config(tls_client)
.connect("nats://localhost")
.await?;
mTLS (Mutual TLS)
let client = ConnectOptions::new()
.add_root_certificates("ca.pem".into())
.add_client_certificate("cert.pem".into(), "key.pem".into())
.connect("tls://localhost")
.await?;
WebSocket Transport
Requires the websockets feature. Supports ws:// and wss:// schemes.
let client = connect("ws://localhost:8080").await?;
let client = connect("wss://localhost:443").await?;
Implementation uses tokio-websockets with a WebSocketAdapter that wraps the WebSocket stream to implement AsyncRead + AsyncWrite:
// WebSocketAdapter bridges WebSocket messages to byte streams
pub(crate) struct WebSocketAdapter<T> {
pub(crate) inner: WebSocketStream<T>,
pub(crate) read_buf: BytesMut, // Buffered incoming WebSocket messages
}
For wss://, TLS is configured within the WebSocket connector, not via the client's TLS layer.
Security Considerations
Nonce Signing
The server's nonce in the INFO message prevents replay attacks:
- Each connection gets a unique nonce
- The nonce must be signed with the client's private key
- The signature is verified server-side against the public key
Authorization Violations
When the server sends -ERR 'authorization violation':
- The client parses this as
ServerError::AuthorizationViolation - The
Connectorimmediately propagates this error (does not retry) - The error is converted to
ConnectErrorKind::AuthorizationViolation
Subject Validation
By default, the client validates subjects for protocol safety:
- Publish subjects: checked for emptiness and whitespace (can be disabled with
skip_subject_validation) - Subscribe subjects: always checked for emptiness, whitespace, leading/trailing dots, consecutive dots
- Queue group names: checked for emptiness and whitespace
The server enforces its own validation, but client-side checks prevent protocol-framing errors.
Max Payload Size
The client checks payload size against the server's max_payload before publishing:
- For plain messages:
payload.len() > max_payload - For messages with headers:
headers.wire_len() + payload.len() > max_payload - Returns
PublishErrorKind::MaxPayloadExceededif exceeded
No Echo
When no_echo is set, the CONNECT message includes echo: false. The server will not deliver messages published by this connection back to its own subscriptions. This prevents feedback loops.
Lame Duck Mode
When the server enters lame duck mode (draining for shutdown):
- Server sends
INFOwithldm: true - Client emits
Event::LameDuckMode - Application should gracefully close or reconnect to another server
The nats-server test harness provides set_lame_duck_mode(server) for testing this behavior.