Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kerberos support for node-postgres #3267

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -10,3 +10,4 @@ dist
/.eslintcache
.vscode/
manually-test-on-heroku.js
.idea/
13 changes: 13 additions & 0 deletions packages/pg-protocol/src/inbound-parser.test.ts
Original file line number Diff line number Diff line change
@@ -178,6 +178,8 @@ var md5PasswordBuffer = buffers.authenticationMD5Password()
var SASLBuffer = buffers.authenticationSASL()
var SASLContinueBuffer = buffers.authenticationSASLContinue()
var SASLFinalBuffer = buffers.authenticationSASLFinal()
var GSSInitBuffer = buffers.authenticationGSSInit()
var GSSContinueBuffer = buffers.authenticationGSSContinue()

var expectedPlainPasswordMessage = {
name: 'authenticationCleartextPassword',
@@ -203,6 +205,15 @@ var expectedSASLFinalMessage = {
data: 'data',
}

var expectedGSSInitMessage = {
name: 'GSSInit',
}

var expectedGSSContinueMessage = {
name: 'GSSContinue',
inToken: btoa('data'),
}

var notificationResponseBuffer = buffers.notification(4, 'hi', 'boom')
var expectedNotificationResponseMessage = {
name: 'notification',
@@ -228,6 +239,8 @@ describe('PgPacketStream', function () {
testForMessage(md5PasswordBuffer, expectedMD5PasswordMessage)
testForMessage(SASLBuffer, expectedSASLMessage)
testForMessage(SASLContinueBuffer, expectedSASLContinueMessage)
testForMessage(GSSInitBuffer, expectedGSSInitMessage)
testForMessage(GSSContinueBuffer, expectedGSSContinueMessage)

// this exercises a found bug in the parser:
// https://github.com/brianc/node-postgres/pull/2210#issuecomment-627626084
8 changes: 8 additions & 0 deletions packages/pg-protocol/src/outbound-serializer.test.ts
Original file line number Diff line number Diff line change
@@ -264,6 +264,14 @@ describe('serializer', () => {
})
})

describe('send binary password', function () {
it('builds sendBinaryPassword', () => {
const actual = serialize.sendBinaryPassword(Buffer.from([1, 2, 3]))
const expected = new BufferList().add(Buffer.from([1, 2, 3])).join(true, 'p')
assert.deepEqual(actual, expected)
})
})

it('builds cancel message', () => {
const actual = serialize.cancel(3, 4)
const expected = new BufferList().addInt16(1234).addInt16(5678).addInt32(3).addInt32(4).join(true)
7 changes: 7 additions & 0 deletions packages/pg-protocol/src/parser.ts
Original file line number Diff line number Diff line change
@@ -327,6 +327,13 @@ export class Parser {
return new AuthenticationMD5Password(length, salt)
}
break
case 7: // GSS Init (Kerberos)
message.name = 'GSSInit'
break
case 8: // GSSAPI Continue (Kerberos)
message.name = 'GSSContinue'
message.inToken = this.reader.bytes(length - 8).toString('base64')
return message
case 10: // AuthenticationSASL
message.name = 'authenticationSASL'
message.mechanisms = []
5 changes: 5 additions & 0 deletions packages/pg-protocol/src/serializer.ts
Original file line number Diff line number Diff line change
@@ -239,6 +239,10 @@ const copyData = (chunk: Buffer): Buffer => {
return writer.add(chunk).flush(code.copyFromChunk)
}

const sendBinaryPassword = (chunk: Buffer): Buffer => {
return writer.add(chunk).flush(code.startup)
}

const copyFail = (message: string): Buffer => {
return cstringMessage(code.copyFail, message)
}
@@ -266,6 +270,7 @@ const serialize = {
sync: () => syncBuffer,
end: () => endBuffer,
copyData,
sendBinaryPassword,
copyDone: () => copyDoneBuffer,
copyFail,
cancel,
8 changes: 8 additions & 0 deletions packages/pg-protocol/src/testing/test-buffers.ts
Original file line number Diff line number Diff line change
@@ -21,6 +21,14 @@ const buffers = {
.join(true, 'R')
},

authenticationGSSInit: function () {
return new BufferList().addInt32(7).join(true, 'R')
},

authenticationGSSContinue: function () {
return new BufferList().addInt32(8).addString('data').join(true, 'R')
},

authenticationSASL: function () {
return new BufferList().addInt32(10).addCString('SCRAM-SHA-256').addCString('').join(true, 'R')
},
37 changes: 37 additions & 0 deletions packages/pg/lib/client.js
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@ class Client extends EventEmitter {
this.database = this.connectionParameters.database
this.port = this.connectionParameters.port
this.host = this.connectionParameters.host
this.principal = this.connectionParameters.principal

// "hiding" the password so it doesn't show up in stack traces
// or if the client is console.logged
@@ -174,6 +175,9 @@ class Client extends EventEmitter {
}

_attachListeners(con) {
// kerberos
con.on('GSSInit', this._handleGSSInit.bind(this))
con.on('GSSContinue', this._handleGSSContinue.bind(this))
// password request handling
con.on('authenticationCleartextPassword', this._handleAuthCleartextPassword.bind(this))
// password request handling
@@ -198,6 +202,39 @@ class Client extends EventEmitter {
con.on('notification', this._handleNotification.bind(this))
}

async _handleGSSInit(msg) {
const kerberos = require('kerberos').Kerberos
try {
this.kclient = await kerberos.initializeClient(`${this.principal}@${this.host}`, {
mechOID: kerberos.GSS_MECH_OID_SPNEGO,
})

// TODO: below this might need to be a recursive loop to step multiple times.
const token = await this.kclient.step('')

const buf = Buffer.from(token, 'base64')
this.connection.sendBinaryPassword(buf)
} catch (e) {
this.emit('error', e)
}
}

async _handleGSSContinue(msg) {
try {
const inToken = msg.inToken
const token = await this.kclient.step(inToken)

// TODO: probably a better way to handle this.
if (token == null) {
return
}
const buf = Buffer.from(token, 'base64')
this.connection.sendBinaryPassword(buf)
} catch (e) {
this.emit('error', e)
}
}

// TODO(bmc): deprecate pgpass "built in" integration since this.password can be a function
// it can be supplied by the user if required - this is a breaking change!
_checkPgPass(cb) {
2 changes: 2 additions & 0 deletions packages/pg/lib/connection-parameters.js
Original file line number Diff line number Diff line change
@@ -65,6 +65,8 @@ class ConnectionParameters {

this.port = parseInt(val('port', config), 10)
this.host = val('host', config)
// Kerberos/GSSAPI service principal
this.principal = val('principal', config)

// "hiding" the password so it doesn't show up in stack traces
// or if the client is console.logged
4 changes: 4 additions & 0 deletions packages/pg/lib/connection.js
Original file line number Diff line number Diff line change
@@ -133,6 +133,10 @@ class Connection extends EventEmitter {
this._send(serialize.password(password))
}

sendBinaryPassword(password) {
this._send(serialize.sendBinaryPassword(password))
}

sendSASLInitialResponseMessage(mechanism, initialResponse) {
this._send(serialize.sendSASLInitialResponseMessage(mechanism, initialResponse))
}
1 change: 1 addition & 0 deletions packages/pg/package.json
Original file line number Diff line number Diff line change
@@ -37,6 +37,7 @@
"wrangler": "3.58.0"
},
"optionalDependencies": {
"kerberos": "^2.2.0",
"pg-cloudflare": "^1.1.1"
},
"peerDependencies": {
Original file line number Diff line number Diff line change
@@ -83,6 +83,7 @@ suite.test('ConnectionParameters initializing from config', function () {
lock_timeout: 15000,
idle_in_transaction_session_timeout: 15000,
options: '-c geqo=off',
principal: 'postgres',
}
var subject = new ConnectionParameters(config)
compare(subject, config, 'config')
Original file line number Diff line number Diff line change
@@ -28,13 +28,15 @@ suite.test('ConnectionParameters initialized from environment variables', functi
process.env['PGPORT'] = 7890
process.env['PGDATABASE'] = 'allyerbase'
process.env['PGPASSWORD'] = 'open'
process.env['PGPRINCIPAL'] = 'postgres'

var subject = new ConnectionParameters()
assert.equal(subject.host, 'local', 'env host')
assert.equal(subject.user, 'bmc2', 'env user')
assert.equal(subject.port, 7890, 'env port')
assert.equal(subject.database, 'allyerbase', 'env database')
assert.equal(subject.password, 'open', 'env password')
assert.equal(subject.principal, 'postgres', 'env principal')
})

suite.test('ConnectionParameters initialized from mix', function () {
@@ -49,12 +51,14 @@ suite.test('ConnectionParameters initialized from mix', function () {
var subject = new ConnectionParameters({
user: 'testing',
database: 'zugzug',
principal: 'postgres',
})
assert.equal(subject.host, 'local', 'env host')
assert.equal(subject.user, 'testing', 'config user')
assert.equal(subject.port, 7890, 'env port')
assert.equal(subject.database, 'zugzug', 'config database')
assert.equal(subject.password, defaults.password, 'defaults password')
assert.equal(subject.principal, 'postgres', 'config principal')
})

suite.test('connection string parsing', function () {
214 changes: 183 additions & 31 deletions yarn.lock

Large diffs are not rendered by default.