-
Notifications
You must be signed in to change notification settings - Fork 438
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
feat: ActiveDirectoryPassword and sqlpassword authentication support #707
Changes from 18 commits
6e2d073
8c971f8
b4441fd
6cfa3cd
4d91c4f
511338d
1cad62c
9f691c9
de5e3b8
8a9b876
dd2bbc9
4dd2b1f
fc24867
12a902f
fe0122b
ce3eceb
ce13bdc
c233c74
2cd3240
e36869a
036433d
f6772bf
1ad29ed
eba1623
49558e2
eca2499
62a24fd
ee2714b
3e07782
8df7254
0702b77
eb71906
800e9a4
d63c358
244c05a
c7663fb
80a9313
6b4a86d
cc71fe9
aa4437e
b2645cc
c2168cd
ea96250
1257d5b
d42004e
ca3aba5
78e9460
0a28dd1
ef3860d
9759b65
c183a1b
3f62dbd
2da13de
7e9f341
59e5386
d8c8f97
b212b29
8029edc
851630e
16c8ae6
60e11a7
5ec11aa
34321da
6066810
fc923e4
4515d4c
2325bdb
79ab731
db5dd73
078f384
9b453c1
df4570e
64324d5
d658927
e76668b
42dadb1
263cde1
3b65260
c77cd55
14d3d22
7f602ab
d393e7d
60b64dd
45921fc
f242402
75e8ff4
532b969
544f2e1
9c9ffa5
cd388dc
fffa06a
a10055f
e7b1bb5
d39dfc1
2acfa02
231962e
2837053
19be9f6
4face04
e3a5a89
8a597f9
01a36f8
572e97b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,13 @@ | ||
const deprecate = require('depd')('tedious'); | ||
|
||
const BulkLoad = require('./bulk-load'); | ||
const Debug = require('./debug'); | ||
const EventEmitter = require('events').EventEmitter; | ||
const InstanceLookup = require('./instance-lookup').InstanceLookup; | ||
const TransientErrorLookup = require('./transient-error-lookup.js').TransientErrorLookup; | ||
const TYPE = require('./packet').TYPE; | ||
const PreloginPayload = require('./prelogin-payload'); | ||
const Login7Payload = require('./login7-payload'); | ||
const Login7Payload = require('./login7-payload').Login7Payload; | ||
const FEDAUTH_OPTIONS = require('./login7-payload').FEDAUTH_OPTIONS; | ||
const NTLMResponsePayload = require('./ntlm-payload'); | ||
const Request = require('./request'); | ||
const RpcRequestPayload = require('./rpcrequest-payload'); | ||
|
@@ -20,6 +20,7 @@ const crypto = require('crypto'); | |
const ConnectionError = require('./errors').ConnectionError; | ||
const RequestError = require('./errors').RequestError; | ||
const Connector = require('./connector').Connector; | ||
const AuthenticationContext = require('adal-node').AuthenticationContext; | ||
|
||
// A rather basic state machine for managing a connection. | ||
// Implements something approximating s3.2.1. | ||
|
@@ -79,6 +80,20 @@ class Connection extends EventEmitter { | |
throw new TypeError('Invalid server: ' + config.server); | ||
} | ||
|
||
this.fedAuthInfo = { | ||
ValidFedAuthEnum: { | ||
SqlPassword: 'SQLPASSWORD', | ||
ActiveDirectoryPassword: 'ACTIVEDIRECTORYPASSWORD', | ||
// TODO: ActiveDirectoryIntegrated | ||
}, | ||
method: undefined, | ||
fedAuthLibrary: undefined, | ||
requiredPreLoginResponse: false, | ||
fedAuthInfoRequested: false, | ||
responsePending: false, | ||
token: undefined | ||
}; | ||
|
||
if (config.domain != undefined) { | ||
deprecateNonStringConfigValue('domain', config.domain); | ||
} | ||
|
@@ -481,7 +496,25 @@ class Connection extends EventEmitter { | |
deprecateNonBooleanConfigValue('options.useUTC', config.options.useUTC); | ||
this.config.options.useUTC = config.options.useUTC; | ||
} | ||
deprecateNullConfigValue('options.useUTC', config.options.useUTC); | ||
|
||
// Whenever authentication property is used to specify an authentication method, | ||
// the client will request encryption (the default value of the Encrypt property will be true) | ||
// and it will validate the server certificate (regardless of the encryption setting), unless TrustServerCertificate = true | ||
if (config.options.authentication != undefined) { | ||
// check for valid options | ||
if (!(config.options.authentication.toUpperCase() === this.fedAuthInfo.ValidFedAuthEnum.SqlPassword || | ||
config.options.authentication.toUpperCase() === this.fedAuthInfo.ValidFedAuthEnum.ActiveDirectoryPassword)) { | ||
throw new Error('An invalid authentication method is specified'); | ||
} | ||
if (this.config.options.tdsVersion < '7_4') { | ||
throw new Error(`Azure Active Directory authentication is not supported in the TDS version ${this.config.options.tdsVersion}`); | ||
} | ||
this.config.options.encrypt = true; | ||
this.fedAuthInfo.method = config.options.authentication; | ||
if (!config.options.trustServerCertificate) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Based on MSFT doc "the server certificate will be validated irrespective of the encryption setting unless TrustServerCertificate is set to true". the default in Tedious is true so the mechanism to meet the criteria is to check whether it is set to true explicitly by user or not, and if the user has not set it, change the default to false. the default trustServerCertificate in ADO and JDBC is false and it doesn't change unless it is set in connection string 😺 |
||
this.config.options.trustServerCertificate = false; | ||
} | ||
} | ||
} | ||
|
||
this.reset = this.reset.bind(this); | ||
|
@@ -498,6 +531,7 @@ class Connection extends EventEmitter { | |
this.transactionDescriptors = [new Buffer([0, 0, 0, 0, 0, 0, 0, 0])]; | ||
this.transitionTo(this.STATE.CONNECTING); | ||
|
||
|
||
if (this.config.options.tdsVersion < '7_2') { | ||
// 'beginTransaction', 'commitTransaction' and 'rollbackTransaction' | ||
// events are utilized to maintain inTransaction property state which in | ||
|
@@ -607,6 +641,44 @@ class Connection extends EventEmitter { | |
this.emit('charsetChange', token.newValue); | ||
}); | ||
|
||
this.tokenStreamParser.on('fedAuthInfo', (token) => { | ||
const clientId = '7f98cb04-cd1e-40df-9140-3bf7e2cea4db'; | ||
if (token.fedAuthInfoData.stsurl && token.fedAuthInfoData.spn) { | ||
this.fedAuthInfo.responsePending = true; | ||
var context = new AuthenticationContext(token.fedAuthInfoData.stsurl); | ||
context.acquireTokenWithUsernamePassword(token.fedAuthInfoData.spn, this.config.userName, this.config.password, clientId, (err, tokenResponse) => { | ||
if (err) { | ||
this.fedAuthInfo.responsePending = false; | ||
this.loginError = ConnectionError('Security token could not be authenticated or authorized.', 'EFEDAUTH'); | ||
} else { | ||
this.fedAuthInfo.responsePending = false; | ||
this.fedAuthInfo.token = tokenResponse; | ||
} | ||
}); | ||
} | ||
}); | ||
|
||
this.tokenStreamParser.on('featureExtAck', (token) => { | ||
switch (token.featureId) { | ||
case FEDAUTH_OPTIONS.FEATURE_ID: | ||
if (!this.fedAuthInfo.fedAuthInfoRequested) { | ||
throw new Error('Did not request federated authentication, but received the acknowledgment'); | ||
} | ||
switch (this.fedAuthInfo.fedAuthLibrary) { | ||
case FEDAUTH_OPTIONS.LIBRARY_ADAL: | ||
if (0 !== token.featureAckDataLen) { | ||
throw new Error(`Federated authentication acknowledgment for ${this.fedAuthInfo.method} authentication method includes extra data`); | ||
} | ||
break; | ||
default: | ||
throw new Error('Attempting to use unknown federated authentication library'); | ||
} | ||
break; | ||
default: | ||
throw new Error('Received acknowledgement for unknown feature'); | ||
} | ||
}); | ||
|
||
this.tokenStreamParser.on('loginack', (token) => { | ||
if (!token.tdsVersion) { | ||
// unsupported TDS version | ||
|
@@ -973,7 +1045,8 @@ class Connection extends EventEmitter { | |
|
||
sendPreLogin() { | ||
const payload = new PreloginPayload({ | ||
encrypt: this.config.options.encrypt | ||
encrypt: this.config.options.encrypt, | ||
fedAuthRequested: (this.fedAuthInfo.method !== undefined) | ||
}); | ||
this.messageIo.sendMessage(TYPE.PRELOGIN, payload.data); | ||
this.debug.payload(function() { | ||
|
@@ -994,7 +1067,13 @@ class Connection extends EventEmitter { | |
this.debug.payload(function() { | ||
return preloginPayload.toString(' '); | ||
}); | ||
|
||
if (this.fedAuthInfo.method != undefined) { | ||
if (0 !== preloginPayload.fedAuthRequired && 1 !== preloginPayload.fedAuthRequired) { | ||
this.emit('connect', ConnectionError(`Server sent an unexpected response for federated authentication value during negotiation. Value was ${preloginPayload.fedAuthRequired}`, 'EFEDAUTH')); | ||
return this.close(); | ||
} | ||
this.fedAuthInfo.requiredPreLoginResponse = (preloginPayload.fedAuthRequired == 1); | ||
} | ||
if (preloginPayload.encryptionString === 'ON' || preloginPayload.encryptionString === 'REQ') { | ||
if (!this.config.options.encrypt) { | ||
this.emit('connect', ConnectionError("Server requires encryption, set 'encrypt' config option to true.", 'EENCRYPT')); | ||
|
@@ -1021,7 +1100,8 @@ class Connection extends EventEmitter { | |
initDbFatal: !this.config.options.fallbackToDefaultDb, | ||
readOnlyIntent: this.config.options.readOnlyIntent, | ||
sspiBlob: clientResponse, | ||
language: this.config.options.language | ||
language: this.config.options.language, | ||
fedAuthInfo: this.fedAuthInfo | ||
}); | ||
|
||
this.routingData = undefined; | ||
|
@@ -1058,6 +1138,22 @@ class Connection extends EventEmitter { | |
process.nextTick(boundTransitionTo, this.STATE.SENT_NTLM_RESPONSE); | ||
} | ||
|
||
sendFedAuthResponsePacket() { | ||
const accessTokenLen = Buffer.byteLength(this.fedAuthInfo.token.accessToken, 'ucs2'); | ||
const data = new Buffer(8 + accessTokenLen); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you replace Buffer() with Buffer.alloc()? |
||
let offset = 0; | ||
data.writeUInt32LE(accessTokenLen + 4, offset); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
let offset = 0;
offset = data.writeUInt32LE(accessTokenLen + 4, offset);
offset = data.writeUInt32LE(accessTokenLen, offset);
data.write(this.fedAuthInfo.token.accessToken, offset, 'ucs2'); |
||
offset += 4; | ||
data.writeUInt32LE(accessTokenLen, offset); | ||
offset += 4; | ||
data.write(this.fedAuthInfo.token.accessToken, offset, 'ucs2'); | ||
this.messageIo.sendMessage(TYPE.FEDAUTH_TOKEN, data); | ||
// sent the fedAuth token message, the rest is similar to standard login 7 | ||
process.nextTick(() => { | ||
this.transitionTo(this.STATE.SENT_LOGIN7_WITH_STANDARD_LOGIN); | ||
}); | ||
} | ||
|
||
// Returns false to apply backpressure. | ||
sendDataToTokenStreamParser(data) { | ||
return this.tokenStreamParser.addBuffer(data); | ||
|
@@ -1216,6 +1312,19 @@ class Connection extends EventEmitter { | |
} | ||
} | ||
|
||
processLogin7FedAuthResponse() { | ||
if (this.fedAuthInfo.fedAuthInfoRequested && !this.loginError) { | ||
return this.dispatchEvent('receivedFedAuthInfo'); | ||
} else { | ||
if (this.loginError) { | ||
this.emit('connect', this.loginError); | ||
} else { | ||
this.emit('connect', ConnectionError('Login failed.', 'ELOGIN')); | ||
} | ||
return this.dispatchEvent('loginFailed'); | ||
} | ||
} | ||
|
||
execSqlBatch(request) { | ||
this.makeRequest(request, TYPE.SQL_BATCH, new SqlBatchPayload(request.sqlTextOrProcedure, this.currentTransactionDescriptor(), this.config.options)); | ||
} | ||
|
@@ -1621,10 +1730,12 @@ Connection.prototype.STATE = { | |
message: function() { | ||
if (this.messageIo.tlsNegotiationComplete) { | ||
this.sendLogin7Packet(() => { | ||
if (this.config.domain) { | ||
this.transitionTo(this.STATE.SENT_LOGIN7_WITH_NTLM); | ||
if (this.fedAuthInfo.requiredPreLoginResponse) { | ||
return this.transitionTo(this.STATE.SENT_LOGIN7_WITH_FEDAUTH); | ||
} else if (this.config.domain) { | ||
return this.transitionTo(this.STATE.SENT_LOGIN7_WITH_NTLM); | ||
} else { | ||
this.transitionTo(this.STATE.SENT_LOGIN7_WITH_STANDARD_LOGIN); | ||
return this.transitionTo(this.STATE.SENT_LOGIN7_WITH_STANDARD_LOGIN); | ||
Suraiya-Hameed marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
}); | ||
} | ||
|
@@ -1706,6 +1817,47 @@ Connection.prototype.STATE = { | |
} | ||
} | ||
}, | ||
SENT_LOGIN7_WITH_FEDAUTH: { | ||
name: 'SentLogin7Withfedauth', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you split this into 2 states, as per There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
events: { | ||
socketError: function() { | ||
return this.transitionTo(this.STATE.FINAL); | ||
}, | ||
connectTimeout: function() { | ||
return this.transitionTo(this.STATE.FINAL); | ||
}, | ||
data: function(data) { | ||
if (this.fedAuthInfo.responsePending) { | ||
// We got data from the server while we're waiting for adal authentication context | ||
// call to complete on the client. We cannot process server data | ||
// until this call completes as the state can change on completion of | ||
// the call. Queue it for later. | ||
const boundDispatchEvent = this.dispatchEvent.bind(this); | ||
return setImmediate(boundDispatchEvent, 'data', data); | ||
} else { | ||
return this.sendDataToTokenStreamParser(data); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we don't need this |
||
} | ||
}, | ||
receivedFedAuthInfo: function() { | ||
return this.sendFedAuthResponsePacket(); | ||
}, | ||
loginFailed: function() { | ||
return this.transitionTo(this.STATE.FINAL); | ||
}, | ||
message: function() { | ||
if (this.fedAuthInfo.responsePending) { | ||
// We got data from the server while we're waiting for adal authentication context | ||
// call to complete on the client. We cannot process server data | ||
// until this call completes as the state can change on completion of | ||
// the call. Queue it for later. | ||
const boundDispatchEvent = this.dispatchEvent.bind(this); | ||
return setImmediate(boundDispatchEvent, 'message'); | ||
} else { | ||
return this.processLogin7FedAuthResponse(); | ||
} | ||
} | ||
} | ||
}, | ||
LOGGED_IN_SENDING_INITIAL_SQL: { | ||
name: 'LoggedInSendingInitialSql', | ||
enter: function() { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SqlPassword
authentication seems to fail, can you remove it?