Skip to content

Commit

Permalink
feat: add AAD authentication support to connection strings
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
Shin-Aska authored and dhensby committed Aug 8, 2023
1 parent fdf7b17 commit 7b22303
Show file tree
Hide file tree
Showing 4 changed files with 524 additions and 41 deletions.
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
55 changes: 55 additions & 0 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 @@ -204,6 +239,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 +276,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 Down
92 changes: 51 additions & 41 deletions lib/tedious/connection-pool.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,56 @@ 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: {
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
}
}, 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 +68,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

0 comments on commit 7b22303

Please sign in to comment.