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

  1. Verify the password is correct — connect with the same credentials via psql to confirm.
  2. If using a connection pooler (PgBouncer, etc.), ensure it is configured for SCRAM auth passthrough or use session mode.
  3. Check whether the password was recently rotated on the server and update your connection config.
  4. 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

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


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)