jstedfast/MailKit · error · SaslException
InvalidChallenge
InvalidChallenge
Error message
Challenge contained an invalid nonce.
What it means
After extracting the SCRAM server nonce ('r'), MailKit verifies that it starts with the client nonce sent in the client-first message. If not, SaslException with SaslErrorCode.InvalidChallenge and "Challenge contained an invalid nonce" is thrown (SaslMechanismScramBase.cs:354). RFC 5802 requires server_nonce = client_nonce + server_nonce, so a mismatch indicates a broken or malicious server.
Solutions
- Capture the client-first and server-first messages (enable protocol logging, e.g. IProtocolLogger) and compare nonce prefixes
- Confirm the server implements RFC 5802 nonce concatenation; upgrade or patch the server
- Rule out MITM/replay by using TLS for the connection
- Fall back to another mechanism if the server's SCRAM nonce logic is broken
Example fix
// before
client.Authenticate (new SaslMechanismScramSha256 ("user", "pass")); // non-conformant server nonce
// after
try {
client.Authenticate (new SaslMechanismScramSha256 ("user", "pass"));
} catch (SaslException ex) when (ex.ErrorCode == SaslErrorCode.InvalidChallenge && ex.Message.Contains ("nonce")) {
client.Authenticate (new SaslMechanismPlain ("user", "pass")); // over TLS
} Defensive patterns
Strategy: try-catch
Validate before calling
// Enforce TLS so nonce mismatches cannot come from tampering:
if (!client.IsSecure)
await client.ConnectAsync (host, port, SecureSocketOptions.SslOnConnect); Try / catch
try {
client.Authenticate (scramMechanism);
} catch (SaslException ex) when (ex.ErrorCode == SaslErrorCode.InvalidChallenge &&
ex.Message.Contains ("invalid nonce")) {
// server nonce does not extend the client nonce
} Prevention
- Always use SCRAM over TLS (protects the nonce exchange)
- Verify server RFC 5802 compliance during integration testing
- Use protocol logging to compare client vs server nonces
- Treat repeated nonce failures as a possible MITM indicator
When it happens
Trigger: Calling Challenge() on a SCRAM mechanism where the server's r=... value does not begin with the exact cnonce the client generated (e.g. server returns a wholly different nonce, or a replayed stale challenge).
Common situations: Custom/buggy SCRAM server implementations that ignore the client nonce, challenge replay by a man-in-the-middle, or caching proxies serving stale challenges.
Related errors
AI-assisted analysis of jstedfast/MailKit@9d3859a785 (2026-09-15).
Data as JSON: /api/errors/b58d3c607faf0413.
Report an issue: GitHub.
Appendix: source
Thrown at MailKit/Security/SaslMechanismScramBase.cs:354
if (token == null)
throw new SaslException (MechanismName, SaslErrorCode.MissingChallenge, "Server response did not contain any authentication data.");
server = Encoding.UTF8.GetString (token, startIndex, length);
var tokens = ParseServerChallenge (server);
string? salt, nonce, iterations;
int count;
if (!tokens.TryGetValue ('s', out salt))
throw new SaslException (MechanismName, SaslErrorCode.IncompleteChallenge, "Challenge did not contain a salt.");
if (!tokens.TryGetValue ('r', out nonce))
throw new SaslException (MechanismName, SaslErrorCode.IncompleteChallenge, "Challenge did not contain a nonce.");
if (!tokens.TryGetValue ('i', out iterations))
throw new SaslException (MechanismName, SaslErrorCode.IncompleteChallenge, "Challenge did not contain an iteration count.");
if (!nonce.StartsWith (cnonce!, StringComparison.Ordinal))
throw new SaslException (MechanismName, SaslErrorCode.InvalidChallenge, "Challenge contained an invalid nonce.");
if (!int.TryParse (iterations, NumberStyles.None, CultureInfo.InvariantCulture, out count) || count < 1)
throw new SaslException (MechanismName, SaslErrorCode.InvalidChallenge, "Challenge contained an invalid iteration count.");
var password = Encoding.UTF8.GetBytes (SaslPrep (Credentials.Password));
salted = Hi (password, Convert.FromBase64String (salt), count);
Array.Clear (password, 0, password.Length);
input = GetChannelBindingInput (channelBindingKind, AuthorizationId);
var inputBuffer = Encoding.ASCII.GetBytes (input);
string base64;
if (SupportsChannelBinding && channelBindingToken != null) {
var binding = new byte[inputBuffer.Length + channelBindingToken.Length];
Buffer.BlockCopy (inputBuffer, 0, binding, 0, inputBuffer.Length);
Buffer.BlockCopy (channelBindingToken, 0, binding, inputBuffer.Length, channelBindingToken.Length);
View on GitHub (pinned to 9d3859a785)