Skip to content

Commit 59aea21

Browse files
Shin-Askadhensby
authored andcommitted
feat: add AAD authentication support to connection strings
Authentication now uses the standard values defined on [.NET Platform Extension 7](https://learn.microsoft.com/en-us/dotnet/api/system.data.sqlclient.sqlconnection.connectionstring?view=dotnet-plat-ext-7.0).
1 parent 0c9ce86 commit 59aea21

File tree

4 files changed

+479
-48
lines changed

4 files changed

+479
-48
lines changed

README.md

+39
Original file line numberDiff line numberDiff line change
@@ -574,11 +574,50 @@ In addition to configuration object there is an option to pass config as a conne
574574

575575
##### Classic Connection String
576576

577+
###### Standard configuration using tedious driver
578+
577579
```
578580
Server=localhost,1433;Database=database;User Id=username;Password=password;Encrypt=true
581+
```
582+
###### Standard configuration using msnodesqlv8 driver
583+
```
579584
Driver=msnodesqlv8;Server=(local)\INSTANCE;Database=database;UID=DOMAIN\username;PWD=password;Encrypt=true
580585
```
581586

587+
##### Azure Active Directory Authentication Connection String
588+
589+
Several types of Azure Authentication are supported:
590+
591+
###### Authentication using Active Directory Integrated
592+
```
593+
Server=*.database.windows.net;Database=database;Authentication=Active Directory Integrated;Client secret=clientsecret;Client Id=clientid;Tenant Id=tenantid;Encrypt=true
594+
```
595+
Note: Internally, the 'Active Directory Integrated' will change its type depending on the other parameters you add to it. On the example above, it will change to azure-active-directory-service-principal-secret because we supplied a Client Id, Client secret and Tenant Id.
596+
597+
If you want to utilize Authentication tokens (azure-active-directory-access-token) Just remove the unnecessary additional parameters and supply only a token parameter, such as in this example:
598+
599+
```
600+
Server=*.database.windows.net;Database=database;Authentication=Active Directory Integrated;token=token;Encrypt=true
601+
```
602+
603+
Finally if you want to utilize managed identity services such as managed identity service app service you can follow this example below:
604+
```
605+
Server=*.database.windows.net;Database=database;Authentication=Active Directory Integrated;msi endpoint=msiendpoint;Client Id=clientid;msi secret=msisecret;Encrypt=true
606+
```
607+
or if its managed identity service virtual machines, then follow this:
608+
```
609+
Server=*.database.windows.net;Database=database;Authentication=Active Directory Integrated;msi endpoint=msiendpoint;Client Id=clientid;Encrypt=true
610+
```
611+
612+
We can also utilizes Active Directory Password but unlike the previous examples, it is not part of the Active Directory Integrated Authentication.
613+
614+
###### Authentication using Active Directory Password
615+
```
616+
Server=*.database.windows.net;Database=database;Authentication=Active Directory Password;User Id=username;Password=password;Client Id=clientid;Tenant Id=tenantid;Encrypt=true
617+
```
618+
619+
For more reference, you can consult [here](https://tediousjs.github.io/tedious/api-connection.html#function_newConnection). Under the authentication.type parameter.
620+
582621
## Drivers
583622

584623
### Tedious

lib/base/connection-pool.js

+70-7
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,28 @@ class ConnectionPool extends EventEmitter {
9999
return this._parseConnectionString(connectionString)
100100
}
101101

102+
static _parseAuthenticationType (type, entries) {
103+
switch (type.toLowerCase()) {
104+
case 'active directory integrated':
105+
if (entries.includes('token')) {
106+
return 'azure-active-directory-access-token'
107+
} else if (['client id', 'client secret', 'tenant id'].every(entry => entries.includes(entry))) {
108+
return 'azure-active-directory-service-principal-secret'
109+
} else if (['client id', 'msi endpoint', 'msi secret'].every(entry => entries.includes(entry))) {
110+
return 'azure-active-directory-msi-app-service'
111+
} else if (['client id', 'msi endpoint'].every(entry => entries.includes(entry))) {
112+
return 'azure-active-directory-msi-vm'
113+
}
114+
return 'azure-active-directory-default'
115+
case 'active directory password':
116+
return 'azure-active-directory-password'
117+
case 'ntlm':
118+
return 'ntlm'
119+
default:
120+
return 'default'
121+
}
122+
}
123+
102124
static _parseConnectionString (connectionString) {
103125
const parsed = parseSqlConnectionString(connectionString, true, true)
104126
return Object.entries(parsed).reduce((config, [key, value]) => {
@@ -115,6 +137,9 @@ class ConnectionPool extends EventEmitter {
115137
case 'attachdbfilename':
116138
break
117139
case 'authentication':
140+
Object.assign(config, {
141+
authentication_type: this._parseAuthenticationType(value, Object.keys(parsed))
142+
})
118143
break
119144
case 'column encryption setting':
120145
break
@@ -134,6 +159,16 @@ class ConnectionPool extends EventEmitter {
134159
break
135160
case 'context connection':
136161
break
162+
case 'client id':
163+
Object.assign(config, {
164+
clientId: value
165+
})
166+
break
167+
case 'client secret':
168+
Object.assign(config, {
169+
clientSecret: value
170+
})
171+
break
137172
case 'current language':
138173
Object.assign(config.options, {
139174
language: value
@@ -173,9 +208,11 @@ class ConnectionPool extends EventEmitter {
173208
port,
174209
server
175210
})
176-
Object.assign(config.options, {
177-
instanceName
178-
})
211+
if (instanceName) {
212+
Object.assign(config.options, {
213+
instanceName
214+
})
215+
}
179216
break
180217
}
181218
case 'encrypt':
@@ -204,6 +241,16 @@ class ConnectionPool extends EventEmitter {
204241
min: value
205242
})
206243
break
244+
case 'msi endpoint':
245+
Object.assign(config, {
246+
msiEndpoint: value
247+
})
248+
break
249+
case 'msi secret':
250+
Object.assign(config, {
251+
msiSecret: value
252+
})
253+
break
207254
case 'multipleactiveresultsets':
208255
break
209256
case 'multisubnetfailover':
@@ -231,6 +278,16 @@ class ConnectionPool extends EventEmitter {
231278
break
232279
case 'replication':
233280
break
281+
case 'tenant id':
282+
Object.assign(config, {
283+
tenantId: value
284+
})
285+
break
286+
case 'token':
287+
Object.assign(config, {
288+
token: value
289+
})
290+
break
234291
case 'transaction binding':
235292
Object.assign(config.options, {
236293
enableImplicitTransactions: value.toLowerCase() === 'implicit unbind'
@@ -253,10 +310,16 @@ class ConnectionPool extends EventEmitter {
253310
domain = domainUser[1]
254311
user = domainUser[2]
255312
}
256-
Object.assign(config, {
257-
domain,
258-
user
259-
})
313+
if (domain) {
314+
Object.assign(config, {
315+
domain
316+
})
317+
}
318+
if (user) {
319+
Object.assign(config, {
320+
user
321+
})
322+
}
260323
break
261324
}
262325
case 'user instance':

lib/tedious/connection-pool.js

+56-41
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,61 @@ const shared = require('../shared')
88
const ConnectionError = require('../error/connection-error')
99

1010
class ConnectionPool extends BaseConnectionPool {
11+
_config () {
12+
const cfg = {
13+
server: this.config.server,
14+
options: Object.assign({
15+
encrypt: typeof this.config.encrypt === 'boolean' ? this.config.encrypt : true,
16+
trustServerCertificate: typeof this.config.trustServerCertificate === 'boolean' ? this.config.trustServerCertificate : false
17+
}, this.config.options),
18+
authentication: Object.assign({
19+
type: this.config.domain !== undefined ? 'ntlm' : this.config.authentication_type !== undefined ? this.config.authentication_type : 'default',
20+
options: Object.entries({
21+
userName: this.config.user,
22+
password: this.config.password,
23+
domain: this.config.domain,
24+
clientId: this.config.clientId,
25+
clientSecret: this.config.clientSecret,
26+
tenantId: this.config.tenantId,
27+
token: this.config.token,
28+
msiEndpoint: this.config.msiEndpoint,
29+
msiSecret: this.config.msiSecret
30+
}).reduce((acc, [key, val]) => {
31+
if (typeof val !== 'undefined') {
32+
return { ...acc, [key]: val }
33+
}
34+
return acc
35+
}, {})
36+
}, this.config.authentication)
37+
}
38+
39+
cfg.options.database = cfg.options.database || this.config.database
40+
cfg.options.port = cfg.options.port || this.config.port
41+
cfg.options.connectTimeout = cfg.options.connectTimeout ?? this.config.connectionTimeout ?? this.config.timeout ?? 15000
42+
cfg.options.requestTimeout = cfg.options.requestTimeout ?? this.config.requestTimeout ?? this.config.timeout ?? 15000
43+
cfg.options.tdsVersion = cfg.options.tdsVersion || '7_4'
44+
cfg.options.rowCollectionOnDone = cfg.options.rowCollectionOnDone || false
45+
cfg.options.rowCollectionOnRequestCompletion = cfg.options.rowCollectionOnRequestCompletion || false
46+
cfg.options.useColumnNames = cfg.options.useColumnNames || false
47+
cfg.options.appName = cfg.options.appName || 'node-mssql'
48+
49+
// tedious always connect via tcp when port is specified
50+
if (cfg.options.instanceName) delete cfg.options.port
51+
52+
if (isNaN(cfg.options.requestTimeout)) cfg.options.requestTimeout = 15000
53+
if (cfg.options.requestTimeout === Infinity || cfg.options.requestTimeout < 0) cfg.options.requestTimeout = 0
54+
55+
if (!cfg.options.debug && this.config.debug) {
56+
cfg.options.debug = {
57+
packet: true,
58+
token: true,
59+
data: true,
60+
payload: true
61+
}
62+
}
63+
return cfg
64+
}
65+
1166
_poolCreate () {
1267
return new shared.Promise((resolve, reject) => {
1368
const resolveOnce = (v) => {
@@ -18,49 +73,9 @@ class ConnectionPool extends BaseConnectionPool {
1873
reject(e)
1974
resolve = reject = () => {}
2075
}
21-
const cfg = {
22-
server: this.config.server,
23-
options: Object.assign({
24-
encrypt: typeof this.config.encrypt === 'boolean' ? this.config.encrypt : true,
25-
trustServerCertificate: typeof this.config.trustServerCertificate === 'boolean' ? this.config.trustServerCertificate : false
26-
}, this.config.options),
27-
authentication: Object.assign({
28-
type: this.config.domain !== undefined ? 'ntlm' : 'default',
29-
options: {
30-
userName: this.config.user,
31-
password: this.config.password,
32-
domain: this.config.domain
33-
}
34-
}, this.config.authentication)
35-
}
36-
37-
cfg.options.database = cfg.options.database || this.config.database
38-
cfg.options.port = cfg.options.port || this.config.port
39-
cfg.options.connectTimeout = cfg.options.connectTimeout ?? this.config.connectionTimeout ?? this.config.timeout ?? 15000
40-
cfg.options.requestTimeout = cfg.options.requestTimeout ?? this.config.requestTimeout ?? this.config.timeout ?? 15000
41-
cfg.options.tdsVersion = cfg.options.tdsVersion || '7_4'
42-
cfg.options.rowCollectionOnDone = cfg.options.rowCollectionOnDone || false
43-
cfg.options.rowCollectionOnRequestCompletion = cfg.options.rowCollectionOnRequestCompletion || false
44-
cfg.options.useColumnNames = cfg.options.useColumnNames || false
45-
cfg.options.appName = cfg.options.appName || 'node-mssql'
46-
47-
// tedious always connect via tcp when port is specified
48-
if (cfg.options.instanceName) delete cfg.options.port
49-
50-
if (isNaN(cfg.options.requestTimeout)) cfg.options.requestTimeout = 15000
51-
if (cfg.options.requestTimeout === Infinity || cfg.options.requestTimeout < 0) cfg.options.requestTimeout = 0
52-
53-
if (!cfg.options.debug && this.config.debug) {
54-
cfg.options.debug = {
55-
packet: true,
56-
token: true,
57-
data: true,
58-
payload: true
59-
}
60-
}
6176
let tedious
6277
try {
63-
tedious = new tds.Connection(cfg)
78+
tedious = new tds.Connection(this._config())
6479
} catch (err) {
6580
rejectOnce(err)
6681
return

0 commit comments

Comments
 (0)