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
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -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
Expand Down
8 changes: 8 additions & 0 deletions packages/pg-protocol/src/outbound-serializer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions packages/pg-protocol/src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down
5 changes: 5 additions & 0 deletions packages/pg-protocol/src/serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -266,6 +270,7 @@ const serialize = {
sync: () => syncBuffer,
end: () => endBuffer,
copyData,
sendBinaryPassword,
copyDone: () => copyDoneBuffer,
copyFail,
cancel,
Expand Down
8 changes: 8 additions & 0 deletions packages/pg-protocol/src/testing/test-buffers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
},
Expand Down
37 changes: 37 additions & 0 deletions packages/pg/lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand Down
2 changes: 2 additions & 0 deletions packages/pg/lib/connection-parameters.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions packages/pg/lib/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down
1 change: 1 addition & 0 deletions packages/pg/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"wrangler": "3.58.0"
},
"optionalDependencies": {
"kerberos": "^2.2.0",
"pg-cloudflare": "^1.1.1"
},
"peerDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand All @@ -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 () {
Expand Down
Loading