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

Fixed issues related to previous PR regarding AAD authentication via connection string #1461

Merged
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
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -574,11 +574,50 @@ In addition to configuration object there is an option to pass config as a conne

##### Classic Connection String

###### Standard configuration using tedious driver

```
Server=localhost,1433;Database=database;User Id=username;Password=password;Encrypt=true
```
###### Standard configuration using msnodesqlv8 driver
```
Driver=msnodesqlv8;Server=(local)\INSTANCE;Database=database;UID=DOMAIN\username;PWD=password;Encrypt=true
```

##### Azure Active Directory Authentication Connection String

Several types of Azure Authentication are supported:

###### Authentication using Active Directory Integrated
```
Server=*.database.windows.net;Database=database;Authentication=Active Directory Integrated;Client secret=clientsecret;Client Id=clientid;Tenant Id=tenantid;Encrypt=true
```
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.

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:

```
Server=*.database.windows.net;Database=database;Authentication=Active Directory Integrated;token=token;Encrypt=true
```

Finally if you want to utilize managed identity services such as managed identity service app service you can follow this example below:
```
Server=*.database.windows.net;Database=database;Authentication=Active Directory Integrated;msi endpoint=msiendpoint;Client Id=clientid;msi secret=msisecret;Encrypt=true
```
or if its managed identity service virtual machines, then follow this:
```
Server=*.database.windows.net;Database=database;Authentication=Active Directory Integrated;msi endpoint=msiendpoint;Client Id=clientid;Encrypt=true
```

We can also utilizes Active Directory Password but unlike the previous examples, it is not part of the Active Directory Integrated Authentication.

###### Authentication using Active Directory Password
```
Server=*.database.windows.net;Database=database;Authentication=Active Directory Password;User Id=username;Password=password;Client Id=clientid;Tenant Id=tenantid;Encrypt=true
```

For more reference, you can consult [here](https://tediousjs.github.io/tedious/api-connection.html#function_newConnection). Under the authentication.type parameter.

## Drivers

### Tedious
Expand Down
77 changes: 70 additions & 7 deletions lib/base/connection-pool.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,28 @@ class ConnectionPool extends EventEmitter {
return this._parseConnectionString(connectionString)
}

static _parseAuthenticationType (type, entries) {
switch (type.toLowerCase()) {
case 'active directory integrated':
if (entries.includes('token')) {
return 'azure-active-directory-access-token'
} else if (['client id', 'client secret', 'tenant id'].every(entry => entries.includes(entry))) {
return 'azure-active-directory-service-principal-secret'
} else if (['client id', 'msi endpoint', 'msi secret'].every(entry => entries.includes(entry))) {
return 'azure-active-directory-msi-app-service'
} else if (['client id', 'msi endpoint'].every(entry => entries.includes(entry))) {
return 'azure-active-directory-msi-vm'
}
return 'azure-active-directory-default'
case 'active directory password':
return 'azure-active-directory-password'
case 'ntlm':
return 'ntlm'
default:
return 'default'
}
}

static _parseConnectionString (connectionString) {
const parsed = parseSqlConnectionString(connectionString, true, true)
return Object.entries(parsed).reduce((config, [key, value]) => {
Expand All @@ -115,6 +137,9 @@ class ConnectionPool extends EventEmitter {
case 'attachdbfilename':
break
case 'authentication':
Object.assign(config, {
authentication_type: this._parseAuthenticationType(value, Object.keys(parsed))
})
break
case 'column encryption setting':
break
Expand All @@ -134,6 +159,16 @@ class ConnectionPool extends EventEmitter {
break
case 'context connection':
break
case 'client id':
Object.assign(config, {
clientId: value
})
break
case 'client secret':
Object.assign(config, {
clientSecret: value
})
break
case 'current language':
Object.assign(config.options, {
language: value
Expand Down Expand Up @@ -173,9 +208,11 @@ class ConnectionPool extends EventEmitter {
port,
server
})
Object.assign(config.options, {
instanceName
})
if (instanceName) {
Object.assign(config.options, {
instanceName
})
}
break
}
case 'encrypt':
Expand Down Expand Up @@ -204,6 +241,16 @@ class ConnectionPool extends EventEmitter {
min: value
})
break
case 'msi endpoint':
Object.assign(config, {
msiEndpoint: value
})
break
case 'msi secret':
Object.assign(config, {
msiSecret: value
})
break
case 'multipleactiveresultsets':
break
case 'multisubnetfailover':
Expand Down Expand Up @@ -231,6 +278,16 @@ class ConnectionPool extends EventEmitter {
break
case 'replication':
break
case 'tenant id':
Object.assign(config, {
tenantId: value
})
break
case 'token':
Object.assign(config, {
token: value
})
break
case 'transaction binding':
Object.assign(config.options, {
enableImplicitTransactions: value.toLowerCase() === 'implicit unbind'
Expand All @@ -253,10 +310,16 @@ class ConnectionPool extends EventEmitter {
domain = domainUser[1]
user = domainUser[2]
}
Object.assign(config, {
domain,
user
})
if (domain) {
Object.assign(config, {
domain
})
}
if (user) {
Object.assign(config, {
user
})
}
break
}
case 'user instance':
Expand Down
97 changes: 56 additions & 41 deletions lib/tedious/connection-pool.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,61 @@ const shared = require('../shared')
const ConnectionError = require('../error/connection-error')

class ConnectionPool extends BaseConnectionPool {
_config () {
const cfg = {
server: this.config.server,
options: Object.assign({
encrypt: typeof this.config.encrypt === 'boolean' ? this.config.encrypt : true,
trustServerCertificate: typeof this.config.trustServerCertificate === 'boolean' ? this.config.trustServerCertificate : false
}, this.config.options),
authentication: Object.assign({
type: this.config.domain !== undefined ? 'ntlm' : this.config.authentication_type !== undefined ? this.config.authentication_type : 'default',
options: Object.entries({
userName: this.config.user,
password: this.config.password,
domain: this.config.domain,
clientId: this.config.clientId,
clientSecret: this.config.clientSecret,
tenantId: this.config.tenantId,
token: this.config.token,
msiEndpoint: this.config.msiEndpoint,
msiSecret: this.config.msiSecret
}).reduce((acc, [key, val]) => {
if (typeof val !== 'undefined') {
return { ...acc, [key]: val }
}
return acc
}, {})
}, this.config.authentication)
}

cfg.options.database = cfg.options.database || this.config.database
cfg.options.port = cfg.options.port || this.config.port
cfg.options.connectTimeout = cfg.options.connectTimeout ?? this.config.connectionTimeout ?? this.config.timeout ?? 15000
cfg.options.requestTimeout = cfg.options.requestTimeout ?? this.config.requestTimeout ?? this.config.timeout ?? 15000
cfg.options.tdsVersion = cfg.options.tdsVersion || '7_4'
cfg.options.rowCollectionOnDone = cfg.options.rowCollectionOnDone || false
cfg.options.rowCollectionOnRequestCompletion = cfg.options.rowCollectionOnRequestCompletion || false
cfg.options.useColumnNames = cfg.options.useColumnNames || false
cfg.options.appName = cfg.options.appName || 'node-mssql'

// tedious always connect via tcp when port is specified
if (cfg.options.instanceName) delete cfg.options.port

if (isNaN(cfg.options.requestTimeout)) cfg.options.requestTimeout = 15000
if (cfg.options.requestTimeout === Infinity || cfg.options.requestTimeout < 0) cfg.options.requestTimeout = 0

if (!cfg.options.debug && this.config.debug) {
cfg.options.debug = {
packet: true,
token: true,
data: true,
payload: true
}
}
return cfg
}

_poolCreate () {
return new shared.Promise((resolve, reject) => {
const resolveOnce = (v) => {
Expand All @@ -18,49 +73,9 @@ class ConnectionPool extends BaseConnectionPool {
reject(e)
resolve = reject = () => {}
}
const cfg = {
server: this.config.server,
options: Object.assign({
encrypt: typeof this.config.encrypt === 'boolean' ? this.config.encrypt : true,
trustServerCertificate: typeof this.config.trustServerCertificate === 'boolean' ? this.config.trustServerCertificate : false
}, this.config.options),
authentication: Object.assign({
type: this.config.domain !== undefined ? 'ntlm' : 'default',
options: {
userName: this.config.user,
password: this.config.password,
domain: this.config.domain
}
}, this.config.authentication)
}

cfg.options.database = cfg.options.database || this.config.database
cfg.options.port = cfg.options.port || this.config.port
cfg.options.connectTimeout = cfg.options.connectTimeout ?? this.config.connectionTimeout ?? this.config.timeout ?? 15000
cfg.options.requestTimeout = cfg.options.requestTimeout ?? this.config.requestTimeout ?? this.config.timeout ?? 15000
cfg.options.tdsVersion = cfg.options.tdsVersion || '7_4'
cfg.options.rowCollectionOnDone = cfg.options.rowCollectionOnDone || false
cfg.options.rowCollectionOnRequestCompletion = cfg.options.rowCollectionOnRequestCompletion || false
cfg.options.useColumnNames = cfg.options.useColumnNames || false
cfg.options.appName = cfg.options.appName || 'node-mssql'

// tedious always connect via tcp when port is specified
if (cfg.options.instanceName) delete cfg.options.port

if (isNaN(cfg.options.requestTimeout)) cfg.options.requestTimeout = 15000
if (cfg.options.requestTimeout === Infinity || cfg.options.requestTimeout < 0) cfg.options.requestTimeout = 0

if (!cfg.options.debug && this.config.debug) {
cfg.options.debug = {
packet: true,
token: true,
data: true,
payload: true
}
}
let tedious
try {
tedious = new tds.Connection(cfg)
tedious = new tds.Connection(this._config())
} catch (err) {
rejectOnce(err)
return
Expand Down
Loading