Skip to content

SNOW-3709138: Authentication failures report SQLState 08001 (connection) instead of 28000 (invalid authorization) #2681

Description

@andriivaliukh

TL;DR

Server-side credential‑rejection failures (wrong password, invalid JWT/key‑pair, invalid OAuth token) that reach the login‑failure throw site are thrown as a SnowflakeSQLException with SQLState 08001 ("SQL‑client unable to establish SQL‑connection"). The driver hardcodes this state at that throw site regardless of the server's error code. The correct SQLState for these is 28000 ("invalid authorization specification") — which the driver already uses for locally‑detected missing credentials. Any tooling that classifies errors by SQLState (jOOQ, connection pools, retry frameworks) therefore treats a bad password as a transient connection outage and retries it / surfaces the wrong status.


1. What version of the JDBC driver are you using?

net.snowflake:snowflake-jdbc:4.3.1 (current latest on Maven Central; verified at runtime). The login‑failure throw site described below is present on current master (checked at v4.3.1+).

2. What operating system and processor architecture?

macOS / aarch64. The defect is platform‑independent — it is pure server‑response‑parsing logic.

3. What version of Java are you using?

Java 21.0.11.

4. What did you do? (reproduction)

Connect to a real account with bad credentials and inspect the raw exception. Minimal runnable repro:

import java.sql.*;
import java.util.Properties;

public class SnowflakeAuthSqlStateRepro {
  public static void main(String[] args) {
    String url = "jdbc:snowflake://<account>.snowflakecomputing.com/";
    Properties p = new Properties();
    p.put("user", "<real-existing-user>");
    p.put("password", "definitely-the-wrong-password"); // real user, wrong password
    try (Connection c = DriverManager.getConnection(url, p)) {
      System.out.println("unexpected success");
    } catch (SQLException e) {
      System.out.printf("errorCode=%d  sqlState=%s  msg=%s%n",
          e.getErrorCode(), e.getSQLState(), e.getMessage());
    }
  }
}

Observed output (wrong password):

errorCode=390100  sqlState=08001  msg=Incorrect username or password was specified.

Observed output (key‑pair auth with an unregistered key, authenticator=SNOWFLAKE_JWT):

errorCode=390144  sqlState=08001  msg=JWT token is invalid. [<requestId>]

5. What did you expect to see?

For an authentication failure, the SQLState should be 28000 (INVALID_AUTHORIZATION_SPECIFICATION), not the connection class 08001. SQLState class 28 is the SQL‑standard "invalid authorization specification"; class 08 means the client could not establish a connection — which is misleading when the connection was established and the server actively rejected the credentials.

6. DEBUG logs — the raw server login response

With net.snowflake logging at DEBUG, the verbatim login‑failure response body is:

{
  "responseType" : "LoginResponse",
  "data" : {
    "nextAction" : "RETRY_LOGIN",
    "authnMethod" : "USERNAME_PASSWORD",
    "loginName" : "<USER>",
    "signInOptions" : { }
  },
  "success" : false,
  "message" : "Incorrect username or password was specified.",
  "code" : "390100"
}

Note: the response contains no sqlState field — only the envelope code. So the SQLState is chosen entirely by the driver.


Root cause

src/main/java/net/snowflake/client/internal/core/SessionUtil.java — the login‑failure handler unconditionally stamps the connection SQLState, ignoring the server code:

// ~line 940, after reading code/message from the response envelope
throw new SnowflakeSQLException(
    NO_QUERY_ID,
    errorMessage,
    SqlState.SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION,  // 08001 — hardcoded for ALL login failures
    errorCode);                                            // 390100 / 390144 / ... from the server

The same root cause also affects the external‑browser login flow in SessionUtilExternalBrowser.java (~line 215). That site uses a String error code and a different SnowflakeSQLException constructor, so the fix there is equivalent but not identical to the snippet below.

The inconsistency this creates

The driver already maps locally-detected credential problems to 28000 (ErrorCode.java):

MISSING_USERNAME(200011, SqlState.INVALID_AUTHORIZATION_SPECIFICATION), // 28000
MISSING_PASSWORD(200012, SqlState.INVALID_AUTHORIZATION_SPECIFICATION), // 28000

So a missing password yields 28000, but a wrong password (server‑rejected) yields 08001. Same category of problem, two different SQLState classes. (28000 is the driver's established convention for authorization‑input problems — it is also used by MISSING_SERVER_URL (200026), MISSING_CONNECTION_PROPERTY (200028) and INVALID_CONNECTION_URL (200029).)

Why it matters

SQLState is the portable, vendor‑neutral classifier that generic tooling relies on. With 08001, an authentication failure is indistinguishable from a real connectivity outage, so consumers:

  • retry a permanent auth failure as if it were transient (connection pools, retry frameworks), and
  • surface the wrong status (e.g. jOOQ maps 08001C08_CONNECTION_EXCEPTION; downstream services translate that to 503 Service Unavailable rather than 401/Unauthenticated).

Proposed fix

At the login‑failure throw site, select the SQLState from the server code for known credential‑rejection codes; otherwise keep 08001 unchanged (no regression for non‑auth failures):

private static final Set<Integer> AUTH_REJECTION_GS_CODES = Set.of(
    390100, // INCORRECT_USERNAME_PASSWORD                     — reproduced firsthand
    390144, // JWT_TOKEN_INVALID                              — reproduced firsthand
    // The JWT codes below are candidates derived from Snowflake docs; please confirm:
    394300, // JWT_TOKEN_INVALID_USER_IN_ISSUER              (candidate)
    394301, // JWT_TOKEN_MISSING_ISSUE_OR_EXPIRATION_TIME    (candidate)
    394304, // JWT_TOKEN_INVALID_PUBLIC_KEY_FINGERPRINT_MISMATCH (candidate)
    394305, // JWT_TOKEN_INVALID_ALGORITHM                   (candidate)
    394306, // JWT_TOKEN_INVALID_SIGNATURE                   (candidate)
    390303  // OAUTH_ACCESS_TOKEN_INVALID                    (Constants.java)
);

String sqlState = AUTH_REJECTION_GS_CODES.contains(errorCode)
    ? SqlState.INVALID_AUTHORIZATION_SPECIFICATION       // 28000
    : SqlState.SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION; // 08001 (default, unchanged)

throw new SnowflakeSQLException(NO_QUERY_ID, errorMessage, sqlState, errorCode);
  • SqlState.INVALID_AUTHORIZATION_SPECIFICATION already exists (in net.snowflake:snowflake-common) and is already used by the driver, so no new dependency or constant is required.
  • An equivalent change applies at the external‑browser site (SessionUtilExternalBrowser.java ~line 215), adjusted for its String error code and its different SnowflakeSQLException constructor (arg order / Integer.valueOf).
  • Non‑auth failures keep 08001, so genuine connectivity errors are unaffected.

Codes deliberately excluded (not credential rejection)

  • 390318 OAUTH_ACCESS_TOKEN_EXPIRED — diverted before this throw site (re‑auth path; its non‑re‑auth branch currently surfaces an INTERNAL_ERROR SQLState — a separate matter).
  • 390111 / 390112 session gone / expired, 390195 id‑token invalid — session/token lifecycle, handled on other paths.

Compatibility

This changes only the SQLState string for the listed credential‑rejection codes; the vendor error code returned by getErrorCode() (e.g. 390100) is unchanged, so any consumer keying on the error code keeps working. It is a runtime value, not an API signature — no binary/japicmp break. The one behavioral change is that consumers currently treating 08001 as a transient connection error will now (correctly) see 28000 for these auth failures.

Question for maintainers

Is the list above the complete and correct set of credential‑rejection GS codes (390xxx / 394xxx)? You hold the authoritative error‑code catalog; we derived this set from the docs plus firsthand reproduction of 390100 and 390144. Happy to adjust the set and open the PR.


References

Metadata

Metadata

Labels

bugstatus-pr_pending_mergeA PR is made and is under reviewstatus-triage_doneInitial triage done, will be further handled by the driver team

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions