src/dialects/cloud-auth/ is the shared authentication layer for managed-cloud
dialects: AWS RDS/Aurora IAM, Microsoft Entra ID (Azure SQL, Synapse,
Fabric, Azure Database for PostgreSQL/MySQL, Kusto, Cosmos DB) and Google
Cloud SQL / AlloyDB IAM. It is not a dialect; dialects import it.
It has three layers, each usable on its own:
| Layer | Exports |
|---|---|
| Token providers | rdsIamTokenProvider, entraTokenProvider, googleIamTokenProvider, cachedTokenProvider |
| Driver wiring | pgAuth, mysqlAuth, tediousAuth, tediousTokenCredentialAuth, tokenCredentialFrom |
| Connectors | cloudSqlConnectorOptions, alloyDbConnectorOptions |
| TLS | rdsCaBundle, rdsCaBundleUrl, pgVerifiedTls, mysqlVerifiedTls, tediousVerifiedTls, assessTls |
| Safety | CloudAuthError, redactSecrets, registerSecret |
// Inside the package (a dialect under src/dialects/):
import { rdsIamTokenProvider, pgAuth, pgVerifiedTls, rdsCaBundle } from '../cloud-auth';
The module is not exported from the package root; applications reach it through
the managed dialects' dialectOptions (iamAuth, entra, cloudSql.iam, …).
Each of these pages has a tab per login method under Managed and cloud
variants:
iamAuth: an RDS IAM token per connection (Aurora too)iamAuth over mysql_clear_password (Aurora too)@aws-sdk/dsql-signer token, or your tokenProviderentra with a default, managed-identity or service-principal credential (Managed Instance, Fabric)entra, the principal as the user (MySQL, Cosmos DB for PostgreSQL)credential scoped to the clusteriam: true[!CAUTION] RDS for MariaDB refuses IAM
RDS for MariaDB throws oniamAuthandtokenProvider:
themariadbconnector takes a password fixed when the pool is created, so a
15-minute token would break every connection opened after it expires.
Every cloud SDK is an optional peer dependency, loaded with a literal
require() inside a function the first time a token is needed. Importing
cloud-auth, or constructing a provider, loads nothing. A missing SDK throws
CloudAuthError with code: 'SDK_MISSING', packageName, and the
npm install line.
| Package | Needed for |
|---|---|
@aws-sdk/rds-signer | rdsIamTokenProvider |
@azure/identity | entraTokenProvider (not for credential: { type: 'credential' }) |
google-auth-library | googleIamTokenProvider (not when you pass authClient) |
@google-cloud/cloud-sql-connector | cloudSqlConnectorOptions (not when you pass connector) |
Every provider is a TokenProvider:
type TokenProvider = () => Promise<{ token: string; expiresAt: Date }>;
The built-in ones return a CachedTokenProvider, which:
expiresAt − margin (margin 2 minutes byinvalidate(), peek(), provider and a secret-free description.Wrap your own source (a vault, a sidecar) with cachedTokenProvider(fetch)
to get the same behaviour and use the driver helpers with it.
const provider = rdsIamTokenProvider({
hostname: 'mydb.abc123.us-east-1.rds.amazonaws.com', // the real endpoint, not a Route 53 alias
port: 5432,
username: 'app_user',
region: 'us-east-1', // optional: resolved by the SDK otherwise
// credentials: { accessKeyId, secretAccessKey, sessionToken } | async () => creds,
// profile: 'prod', // used only when credentials is not set
});
@aws-sdk/rds-signer's Signer is built once with { hostname, port, username, region, credentials | profile } and getAuthToken() (no
arguments) signs a token locally. Tokens last 15 minutes; with temporary
credentials that expire sooner, expiresAt is capped to their expiration.
Credentials default to the Node provider chain. Works for RDS and Aurora
PostgreSQL / MySQL / MariaDB and for RDS Proxy. Aurora DSQL uses
@aws-sdk/dsql-signer instead (see the auroradsql dialect).
const provider = entraTokenProvider({
target: 'postgres-flexible', // or scope: '…/.default'
credential: { type: 'default' }, // the default
});
target | Scope |
|---|---|
azure-sql, synapse, fabric | https://database.windows.net/.default |
postgres-flexible, mysql-flexible | https://ossrdbms-aad.database.windows.net/.default |
kusto + kustoClusterUri | https://<cluster>.<region>.kusto.windows.net/.default |
kusto without a cluster | https://kusto.kusto.windows.net/.default |
cosmos | https://cosmos.azure.com/.default |
cosmos-postgres | https://token.postgres.cosmos.azure.com/.default |
Public-cloud values. For sovereign clouds pass scope (e.g.
https://ossrdbms-aad.database.usgovcloudapi.net/.default).
[!WARNING] One audience per token
Microsoft has announced that from January 2027 Azure Database for MySQL
will only accept tokens for theossrdbmsaudience, so do not reuse a token
minted for another scope.
Credentials:
credential | Built with |
|---|---|
{ type: 'default', managedIdentityClientId?, tenantId? } | DefaultAzureCredential |
{ type: 'managed-identity', clientId? | resourceId? } | ManagedIdentityCredential |
{ type: 'client-secret', tenantId, clientId, clientSecret } | ClientSecretCredential |
{ type: 'credential', credential } | your TokenCredential (no SDK loaded) |
Entra tokens are valid for 5–60 minutes; the expiry reported by the
credential is used.
const provider = googleIamTokenProvider({ target: 'cloud-sql' }); // or 'alloydb'
Uses Application Default Credentials (or keyFilename, credentials, or your
own authClient) with the login scope:
| Target | Scope |
|---|---|
cloud-sql | https://www.googleapis.com/auth/sqlservice.login |
alloydb | https://www.googleapis.com/auth/alloydb.login |
The database user is the IAM principal: googleIamDatabaseUser(email, 'postgres') strips a service account's .gserviceaccount.com;
googleIamDatabaseUser(email, 'mysql') keeps the part before @.
The three flows differ in who mints the token and where TLS comes from, but
every one goes through the same cache and the same per-connection hook. The
diagram follows cachedTokenProvider() (core.ts), pgAuth() (drivers.ts)
and cloudSqlConnectorOptions() (google.ts).
sequenceDiagram
participant D as Dialect.connect()
participant P as CachedTokenProvider
participant S as Cloud SDK
participant Drv as pg / mysql2 / tedious
participant DB as Database
D->>Drv: pool config + auth fragment
Drv->>P: new connection: password()
alt cached and before refreshAt
P-->>Drv: cached token
else fetch (one in flight)
alt AWS RDS IAM
P->>S: Signer.getAuthToken()
S-->>P: presigned token, 15 min
else Azure Entra ID
P->>S: credential.getToken(scope)
S-->>P: JWT with expiresOnTimestamp
else Cloud SQL / AlloyDB manual IAM
P->>S: authClient.getAccessToken()
S-->>P: OAuth2 token (ADC)
end
P-->>Drv: validated, registered as secret
end
Drv->>DB: login over verified TLS
The provider hands out the cached token until expiresAt − margin, shares one
in-flight refresh between concurrent callers, and the driver hook (password()
on pg, the mysql_clear_password plugin on mysql2, beforeConnect on
mssql) runs for every new pooled connection, so a pool that outlives a token
keeps logging in.
sequenceDiagram
participant D as Cloud SQL dialect connect()
participant C as Cloud SQL connector
participant G as Google APIs
participant Drv as pg / mysql2 / tedious
participant DB as Cloud SQL instance
D->>C: cloudSqlConnectorOptions(driver, options)
C->>G: instance metadata + ephemeral client certificate
G-->>C: certificate, server CA, IP
C-->>D: { stream } or { connector, encrypt: false }
D->>Drv: pool config with the connector options
Drv->>C: open connection
C->>DB: mTLS tunnel (IAM login inside it when iam is automatic)
DB-->>Drv: session
With the connector there is no token in the client at all for the database
login: the connector authenticates the tunnel with Google credentials and,
for pg and mysql2, logs in as the IAM principal inside it. SQL Server has
no IAM login, so iam with the tedious connector throws UNSUPPORTED.
A token read once when the pool is created works until it expires and then
every new pooled connection fails. Each helper returns the fragment that makes
the driver fetch a current token per new connection. Pass the pool's TLS
settings so the helper can warn when the token would travel without verified
TLS.
| Driver | Mechanism | Dialect call site |
|---|---|---|
pg | password as an async function | PostgresDialect.connect() pool config |
mysql2 | authPlugins.mysql_clear_password (async) | MySQLDialect.connect() pool config |
tedious / mssql | access token + beforeConnect swap | MSSQLDialect.connect() config |
tedious ≥ 18 | authentication.type: 'token-credential' | same |
const ssl = pgVerifiedTls(rdsCaBundle()); // or pgVerifiedTls() for Azure
const pool = new Pool({
host,
port,
database,
user: 'app_user',
ssl,
...pgAuth(provider, { ssl }), // { password: async () => token }
});
Used by: RDS/Aurora PostgreSQL, Azure Database for PostgreSQL, Cloud SQL
PostgreSQL and AlloyDB (manual IAM), Cosmos DB for PostgreSQL.
const ssl = mysqlVerifiedTls(rdsCaBundle());
const pool = mysql.createPool({
host,
port,
database,
user: 'app_user',
ssl,
...mysqlAuth(provider, { ssl }), // { authPlugins: { mysql_clear_password } }
});
mysql2 has no password callback, and the pool's connection event fires only
after login, so neither can refresh a token. RDS IAM (AWSAuthenticationPlugin),
Azure MySQL Entra and Cloud SQL IAM all make the server switch to
mysql_clear_password; mysql2 then calls the plugin, asynchronously, for each
new connection. Do not set password or enableCleartextPlugin. The
plugin refuses (INSECURE_TRANSPORT) to send the token on a connection with no
ssl, no connector stream and no local socketPath.
Used by: RDS/Aurora MySQL and MariaDB, Azure Database for MySQL, Cloud SQL MySQL.
const auth = tediousAuth(provider, { tls: tediousVerifiedTls() });
await auth.prime(); // before creating the pool
const pool = new mssql.ConnectionPool({
server,
database,
options: tediousVerifiedTls(),
authentication: auth.authentication, // no user/password
beforeConnect: auth.beforeConnect,
});
tedious validates authentication.options.token when each Connection is
constructed and reads it again only when it sends the login packet. mssql
deep-clones its config once, when the pool is created, so the clone holds
whatever token existed then; beforeConnect (which mssql calls for each new
pooled connection, before login) writes the current token into that
connection. Reading the token inside the refresh margin starts a background
refresh — no timers. Works with tedious 16 (bundled by mssql 10) and 18.
With tedious ≥ 18 / mssql ≥ 11 there is a fully async alternative:
const pool = new mssql.ConnectionPool({
server,
database,
options: tediousVerifiedTls(),
...tediousTokenCredentialAuth(provider, { tls: tediousVerifiedTls() }),
});
Used by: Azure SQL Database / Managed Instance, Synapse, Fabric.
const cx = await cloudSqlConnectorOptions('pg', {
instanceConnectionName: 'project:region:instance',
ipType: 'PRIVATE', // 'PUBLIC' (default) | 'PRIVATE' | 'PSC'
iam: true, // automatic IAM auth; pg and mysql2 only
});
const pool = new Pool({ ...cx.driverOptions, user: 'sa@project.iam', database: 'app' });
// shutdown: await pool.end(); await cx.close();
driverOptions is { stream } for pg and mysql2, and { connector, encrypt: false } for tedious (merge into tedious/mssql options). The
connector does mTLS itself, so pass 'connector' as the TLS posture to the
driver helpers. Automatic IAM is not available for SQL Server; asking for it
throws UNSUPPORTED before anything loads. close() closes only a connector
this module created.
alloyDbConnectorOptions({ instanceUri, ipType, iam, connector? }) returns
pg options from an AlloyDB connector object with a getOptions({ instanceUri, ipType, authType }) method. Google documents AlloyDB language connectors for
Java, Python and Go, and there is no Node.js connector package on npm, so
nothing is loaded for you: without a connector it throws UNSUPPORTED. Pass
your own connector, or use manual IAM: googleIamTokenProvider({ target: 'alloydb' }) with pgAuth() and TLS (or the AlloyDB Auth Proxy).
[!IMPORTANT] Tokens are bearer credentials
Anyone who intercepts one can log in until it expires. Every driver helper
classifies the TLS config (assessTls) and warns once when it isnoneor
unverified.
| Cloud | Default server-side | What the client needs for verified TLS |
|---|---|---|
| AWS | TLS available; PG 15+ defaults rds.force_ssl=1 | The RDS CA bundle — not in Node's trust store. ssl: pgVerifiedTls(rdsCaBundle()). RDS Proxy uses ACM certificates Node already trusts. mysql2 also accepts its built-in ssl: 'Amazon RDS' profile. |
| Azure | PG/MySQL flexible server require TLS; Azure SQL always encrypts | Public roots (DigiCert / Microsoft), so pgVerifiedTls() / mysqlVerifiedTls() with the default trust store; tedious encrypt: true, trustServerCertificate: false. |
| Connectors: mTLS, always | Use a connector (tls: 'connector'), or for direct connections the instance's server CA, downloaded from the instance. |
tls: 'verify'Three dialects whose service presents a certificate Node does not trust by
default — Supabase (SupabaseTlsMode), MySQL HeatWave
(HeatWaveTlsMode) and AlloyDB (AlloyDBTlsMode) — take a
tls option in dialectOptions with two values:
tls | Without a CA (sslRootCert / tlsCa / ssl) | With a CA |
|---|---|---|
'prefer-verify' (default) | falls back to encrypted-but-unverified TLS and logs one warning | verified |
'verify' | throws at construction rather than fall back | verified |
An explicit ssl of your own always wins over the default. No other dialect
has this option; the managed AWS, Azure and Cloud SQL dialects refuse to send
a token over unverified TLS (INSECURE_TRANSPORT) but leave a password
login to the driver's defaults.
This module does not vendor or download it. Download it once, from AWS:
https://truststore.pki.rds.amazonaws.com/<region>/<region>-bundle.pemrdsCaBundleUrl(region); GovCloud uses truststore.pki.us-gov-west-1.rds.amazonaws.com)Then rdsCaBundle('/path/global-bundle.pem'), or set
PRORM_RDS_CA_BUNDLE=/path/global-bundle.pem and call rdsCaBundle(). It
throws INVALID_CONFIG (with the download URL) if no path is set, the file is
unreadable, or it holds no PEM certificate.
Everything throws CloudAuthError with a code:
| Code | Meaning |
|---|---|
SDK_MISSING | Optional SDK not installed; packageName says which |
TOKEN_FETCH_FAILED | The SDK or credential chain failed |
INVALID_TOKEN | A provider returned no token, no valid expiresAt, or an expired one |
TOKEN_UNAVAILABLE | tediousAuth read before prime() / after the token expired |
INVALID_CONFIG | Bad options |
INSECURE_TRANSPORT | mysql2 plugin refused to send a token without TLS |
UNSUPPORTED | e.g. IAM through the tedious connector |
Tokens never appear in errors or log lines:
redactSecrets() replaces it wherever it appears;ya29. tokens, Bearer headers and password= / token: pairs;CloudAuthError.message and its cause (rebuilt from the original error,JSON.stringify, util.inspect,String() and object spread — only .token returns it.connect(), merge the driver helper's fragment into the pool config, andssl/tls value you give the pool.provider.invalidate() before retrying.disconnect().The base PostgresDialect, MySQLDialect and MSSQLDialect currently build
their pool configs from fixed fields, so a function password,
authPlugins, stream or beforeConnect cannot reach the driver without a
small hook in each base class.