Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/sequelize-keeps-its-ssl-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'seamless-auth-api': patch
---

Keep the TLS options when `DATABASE_URL` carries `sslmode`. `resolveSslOptions` turns `DB_SSL`,
or an `sslmode` on the connection string, into Sequelize's `dialectOptions.ssl`, but Sequelize
then read the same `sslmode` for itself and let pg-connection-string's reading of it replace
those options. A URL carrying the parameter had its certificate verified whatever `DB_SSL` or
`DB_SSL_REJECT_UNAUTHORIZED` said, which against Amazon RDS is a boot failure, and `DB_SSL=false`
could not turn TLS off for it. Sequelize is now constructed with `sslmode` taken out of the URL,
once `resolveSslOptions` has read it. Nothing changes on the discrete `DB_*` path, whose URL
never carries the parameter.
20 changes: 20 additions & 0 deletions src/config/database.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,29 @@ function resolveSslOptions(url) {
return ca ? { ca, rejectUnauthorized } : { rejectUnauthorized };
}

// The connection string to construct Sequelize with. Sequelize reads the query of a
// connection string for itself and lets pg-connection-string's reading of `sslmode`
// replace the `ssl` it was given in `dialectOptions`, so with `sslmode` on the URL the
// options above would be thrown away and the certificate verified whatever `DB_SSL` or
// `DB_SSL_REJECT_UNAUTHORIZED` said. `resolveSslOptions` has read `sslmode` by then, so
// it comes out of the URL here. Migrations are not affected: config.cjs hands
// sequelize-cli discrete fields, never the string.
function withoutSslMode(url) {
const query = url.indexOf('?');
if (query === -1) return url;

const params = new URLSearchParams(url.slice(query + 1));
if (!params.has('sslmode')) return url;

params.delete('sslmode');
const rest = params.toString();
return rest ? `${url.slice(0, query)}?${rest}` : url.slice(0, query);
}

module.exports = {
buildDatabaseUrl,
parseDatabaseUrl,
resolveDatabaseUrl,
resolveSslOptions,
withoutSslMode,
};
1 change: 1 addition & 0 deletions src/config/database.d.cts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ export declare function buildDatabaseUrl(): string;
export declare function parseDatabaseUrl(url: string): DatabaseConnection | null;
export declare function resolveDatabaseUrl(): string | null;
export declare function resolveSslOptions(url: string | null): DatabaseSslOptions | null;
export declare function withoutSslMode(url: string): string;
4 changes: 2 additions & 2 deletions src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import path from 'path';
import { Sequelize } from 'sequelize';
import { fileURLToPath } from 'url';

import { buildDatabaseUrl, resolveSslOptions } from '../config/database.cjs';
import { buildDatabaseUrl, resolveSslOptions, withoutSslMode } from '../config/database.cjs';
import getLogger from '../utils/logger.js';

const logger = getLogger('sequelize');
Expand Down Expand Up @@ -42,7 +42,7 @@ export function getSequelize(): Sequelize {

logger.info(`Using Postgres database (TLS ${ssl ? 'enabled' : 'disabled'})`);

sequelizeInstance = new Sequelize(DATABASE_URL, {
sequelizeInstance = new Sequelize(withoutSslMode(DATABASE_URL), {
logging: enableDbLogging ? (msg) => logger.debug(msg) : false,
...(ssl ? { dialectOptions: { ssl } } : {}),
});
Expand Down
23 changes: 23 additions & 0 deletions tests/unit/config/database.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
parseDatabaseUrl,
resolveDatabaseUrl,
resolveSslOptions,
withoutSslMode,
} from '../../../src/config/database.cjs';

const DB_VARS = [
Expand Down Expand Up @@ -193,3 +194,25 @@ describe('sequelize-cli config', () => {
});
});
});

describe('withoutSslMode', () => {
it('takes sslmode out and leaves the credentials and other parameters as they were', () => {
expect(
withoutSslMode(
'postgres://auth:p%40ss@db.internal:5432/auth_db?sslmode=require&application_name=auth',
),
).toBe('postgres://auth:p%40ss@db.internal:5432/auth_db?application_name=auth');
});

it('leaves no trailing query behind when sslmode was the only parameter', () => {
expect(withoutSslMode('postgres://auth@db.internal:5432/auth_db?sslmode=require')).toBe(
'postgres://auth@db.internal:5432/auth_db',
);
});

it('returns a string without sslmode untouched', () => {
expect(withoutSslMode('postgres://auth@db.internal:5432/auth_db')).toBe(
'postgres://auth@db.internal:5432/auth_db',
);
});
});
6 changes: 4 additions & 2 deletions tests/unit/models/sequelize.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,17 @@ describe('getSequelize database URL building', () => {
);
});

it('uses DB_URI when DATABASE_URL is unset', async () => {
it('uses DB_URI when DATABASE_URL is unset, with its sslmode read and then taken out', async () => {
process.env.DB_URI = 'postgres://user:pass@example.com:5432/auth_db?sslmode=verify-full';

const { getSequelize } = await import('../../../src/models/index.js');

getSequelize();

// Sequelize would read the sslmode itself and overwrite dialectOptions.ssl with
// pg-connection-string's version, so the URL it gets no longer carries it.
expect(sequelizeConstructor).toHaveBeenCalledWith(
process.env.DB_URI,
'postgres://user:pass@example.com:5432/auth_db',
expect.objectContaining({
dialectOptions: { ssl: { rejectUnauthorized: true } },
}),
Expand Down
Loading