brianc/node-postgres · critical · Error
SASL: SCRAM-SERVER-FINAL-MESSAGE: server signature does not…
Error message
SASL: SCRAM-SERVER-FINAL-MESSAGE: server signature does not match
What it means
Thrown in finalizeSession() when the server's signature verifier (v= attribute) does not match the signature the client computed locally. During SCRAM-SHA-256, the client derives a server key from the password and computes an expected HMAC-SHA256 over the full auth message. If the server's signature differs, the server failed to prove it knows the password — meaning either the client sent the wrong password or the exchange was tampered with (MITM).
Solutions
- Verify the password is correct — connect with the same credentials via psql to confirm.
- If using a connection pooler (PgBouncer, etc.), ensure it is configured for SCRAM auth passthrough or use session mode.
- Check whether the password was recently rotated on the server and update your connection config.
- Enable SSL/TLS to prevent MITM tampering; if channel binding is available, ensure the server certificate is valid so SCRAM-SHA-256-PLUS can be negotiated.
Example fix
// before — wrong or stale password
const client = new Client({ host: 'db', user: 'app', password: process.env.DB_PASSWORD })
// after — verify password works via psql first, then use the confirmed value
const client = new Client({ host: 'db', user: 'app', password: 'confirmed-correct-password' }) Defensive patterns
Strategy: try-catch
Validate before calling
// Verify the password works before using it in a connection pool:
const { execSync } = require('child_process')
try {
execSync(`PGPASSWORD=${password} psql -h ${host} -U ${user} -d ${database} -c 'SELECT 1'`, { stdio: 'pipe' })
} catch {
console.error('Password verification failed — credentials are incorrect')
} Try / catch
try {
await client.connect()
} catch (err) {
if (err.message.includes('server signature does not match')) {
// Most likely: wrong password or MITM tampering
throw new Error('Authentication failed: verify password or check for MITM')
}
throw err
} Prevention
- Verify the password with psql before deploying.
- Use a secrets manager to avoid stale or manually-entered passwords.
- When rotating passwords, update all clients simultaneously.
- Enable SSL/TLS to prevent man-in-the-middle attacks on the auth exchange.
- If using PgBouncer, use session mode or verify SCRAM passthrough is configured.
When it happens
Trigger: session.serverSignature (computed at sasl.js:125 as HMAC-SHA256 of the server key over the auth message, base64-encoded) does not equal the serverSignature parsed from the server's final message at line 137. The comparison at line 139 fails.
Common situations: Wrong password supplied in the connection string or config (most common cause); password was changed on the server between connection pool creation and use; a connection pooler like PgBouncer in transaction mode interfering with SCRAM state; a TLS man-in-the-middle altering the auth exchange when channel binding is not in use.
Related errors
- SASL: SCRAM-SERVER-FINAL-MESSAGE: server returned error
- SASL: Invalid attribute pair entry
- SASL: SCRAM-SERVER-FINAL-MESSAGE: server signature is…
- SASL: SCRAM-SERVER-FINAL-MESSAGE: serverData must be a…
- SASL: SCRAM-SERVER-FIRST-MESSAGE: invalid iteration count
AI-assisted analysis of brianc/node-postgres@ff9d775abd (2026-08-11).
Data as JSON: /api/errors/322d45a9882deeb2.
Report an issue: GitHub.
Appendix: source
Thrown at packages/pg/lib/crypto/sasl.js:140
const serverSignatureBytes = await crypto.hmacSha256(serverKey, authMessage)
session.message = 'SASLResponse'
session.serverSignature = Buffer.from(serverSignatureBytes).toString('base64')
session.response = clientFinalMessageWithoutProof + ',p=' + clientProof
}
function finalizeSession(session, serverData) {
if (session.message !== 'SASLResponse') {
throw new Error('SASL: Last message was not SASLResponse')
}
if (typeof serverData !== 'string') {
throw new Error('SASL: SCRAM-SERVER-FINAL-MESSAGE: serverData must be a string')
}
const { serverSignature } = parseServerFinalMessage(serverData)
if (serverSignature !== session.serverSignature) {
throw new Error('SASL: SCRAM-SERVER-FINAL-MESSAGE: server signature does not match')
}
}
/**
* printable = %x21-2B / %x2D-7E
* ;; Printable ASCII except ",".
* ;; Note that any "printable" is also
* ;; a valid "value".
*/
function isPrintableChars(text) {
if (typeof text !== 'string') {
throw new TypeError('SASL: text must be a string')
}
return text
.split('')
.map((_, i) => text.charCodeAt(i))
.every((c) => (c >= 0x21 && c <= 0x2b) || (c >= 0x2d && c <= 0x7e))
}View on GitHub (pinned to ff9d775abd)