Skip to content

Commit 7721dac

Browse files
feat: add MSI authentication support for Azure SQL databases
This implements two new authentication methods specifically targeted at Azure SQL databases, namely `azure-active-directory-msi-vm` and `azure-active-directory-msi-app-service`. Co-Authored-By: Michael Sun <[email protected]>
1 parent 9bb7e98 commit 7721dac

File tree

4 files changed

+221
-63
lines changed

4 files changed

+221
-63
lines changed

package-lock.json

Lines changed: 99 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"tag": "next"
4141
},
4242
"dependencies": {
43-
"adal-node": "^0.1.22",
43+
"@azure/ms-rest-nodeauth": "^2.0.2",
4444
"big-number": "1.0.0",
4545
"bl": "^3.0.0",
4646
"depd": "^2.0.0",

src/connection.js

Lines changed: 89 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const os = require('os');
44
const constants = require('constants');
55
const { createSecureContext } = require('tls');
66

7-
const { AuthenticationContext } = require('adal-node');
7+
const { loginWithUsernamePassword, loginWithVmMSI, loginWithAppServiceMSI } = require('@azure/ms-rest-nodeauth');
88

99
const BulkLoad = require('./bulk-load');
1010
const Debug = require('./debug');
@@ -74,8 +74,8 @@ class Connection extends EventEmitter {
7474
throw new TypeError('The "config.authentication.type" property must be of type string.');
7575
}
7676

77-
if (type !== 'default' && type !== 'ntlm' && type !== 'azure-active-directory-password' && type !== 'azure-active-directory-access-token') {
78-
throw new TypeError('The "type" property must one of "default", "ntlm", "azure-active-directory-password" or "azure-active-directory-access-token".');
77+
if (type !== 'default' && type !== 'ntlm' && type !== 'azure-active-directory-password' && type !== 'azure-active-directory-access-token' && type !== 'azure-active-directory-msi-vm' && type !== 'azure-active-directory-msi-app-service') {
78+
throw new TypeError('The "type" property must one of "default", "ntlm", "azure-active-directory-password", "azure-active-directory-access-token", "azure-active-directory-msi-vm" or "azure-active-directory-msi-app-service".');
7979
}
8080

8181
if (typeof options !== 'object' || options === null) {
@@ -130,6 +130,43 @@ class Connection extends EventEmitter {
130130
token: options.token
131131
}
132132
};
133+
} else if (type === 'azure-active-directory-msi-vm') {
134+
if (options.clientId !== undefined && typeof options.clientId !== 'string') {
135+
throw new TypeError('The "config.authentication.options.clientId" property must be of type string.');
136+
}
137+
138+
if (options.msiEndpoint !== undefined && typeof options.msiEndpoint !== 'string') {
139+
throw new TypeError('The "config.authentication.options.msiEndpoint" property must be of type string.');
140+
}
141+
142+
authentication = {
143+
type: 'azure-active-directory-msi-vm',
144+
options: {
145+
clientId: options.clientId,
146+
msiEndpoint: options.msiEndpoint
147+
}
148+
};
149+
} else if (type === 'azure-active-directory-msi-app-service') {
150+
if (options.clientId !== undefined && typeof options.clientId !== 'string') {
151+
throw new TypeError('The "config.authentication.options.clientId" property must be of type string.');
152+
}
153+
154+
if (options.msiEndpoint !== undefined && typeof options.msiEndpoint !== 'string') {
155+
throw new TypeError('The "config.authentication.options.msiEndpoint" property must be of type string.');
156+
}
157+
158+
if (options.msiSecret !== undefined && typeof options.msiSecret !== 'string') {
159+
throw new TypeError('The "config.authentication.options.msiSecret" property must be of type string.');
160+
}
161+
162+
authentication = {
163+
type: 'azure-active-directory-msi-app-service',
164+
options: {
165+
clientId: options.clientId,
166+
msiEndpoint: options.msiEndpoint,
167+
msiSecret: options.msiSecret
168+
}
169+
};
133170
} else {
134171
if (options.userName !== undefined && typeof options.userName !== 'string') {
135172
throw new TypeError('The "config.authentication.options.userName" property must be of type string.');
@@ -1234,6 +1271,15 @@ class Connection extends EventEmitter {
12341271
};
12351272
break;
12361273

1274+
case 'azure-active-directory-msi-vm':
1275+
case 'azure-active-directory-msi-app-service':
1276+
payload.fedAuth = {
1277+
type: 'ADAL',
1278+
echo: this.fedAuthRequired,
1279+
workflow: 'integrated'
1280+
};
1281+
break;
1282+
12371283
case 'ntlm':
12381284
payload.sspi = createNTLMRequest({ domain: authentication.options.domain });
12391285
break;
@@ -1262,13 +1308,13 @@ class Connection extends EventEmitter {
12621308
});
12631309
}
12641310

1265-
sendFedAuthResponsePacket(tokenResponse) {
1266-
const accessTokenLen = Buffer.byteLength(tokenResponse.accessToken, 'ucs2');
1311+
sendFedAuthTokenMessage(token) {
1312+
const accessTokenLen = Buffer.byteLength(token, 'ucs2');
12671313
const data = Buffer.alloc(8 + accessTokenLen);
12681314
let offset = 0;
12691315
offset = data.writeUInt32LE(accessTokenLen + 4, offset);
12701316
offset = data.writeUInt32LE(accessTokenLen, offset);
1271-
data.write(tokenResponse.accessToken, offset, 'ucs2');
1317+
data.write(token, offset, 'ucs2');
12721318
this.messageIo.sendMessage(TYPE.FEDAUTH_TOKEN, data);
12731319
// sent the fedAuth token message, the rest is similar to standard login 7
12741320
this.transitionTo(this.STATE.SENT_LOGIN7_WITH_STANDARD_LOGIN);
@@ -1892,7 +1938,7 @@ Connection.prototype.STATE = {
18921938

18931939
const { authentication } = this.config;
18941940

1895-
if (authentication.type === 'azure-active-directory-password') {
1941+
if (authentication.type === 'azure-active-directory-password' || authentication.type === 'azure-active-directory-msi-vm' || authentication.type === 'azure-active-directory-msi-app-service') {
18961942
this.transitionTo(this.STATE.SENT_LOGIN7_WITH_FEDAUTH);
18971943
} else if (authentication.type === 'ntlm') {
18981944
this.transitionTo(this.STATE.SENT_LOGIN7_WITH_NTLM);
@@ -1920,7 +1966,7 @@ Connection.prototype.STATE = {
19201966
},
19211967
featureExtAck: function(token) {
19221968
const { authentication } = this.config;
1923-
if (authentication.type === 'azure-active-directory-password' || authentication.type === 'azure-active-directory-access-token') {
1969+
if (authentication.type === 'azure-active-directory-password' || authentication.type === 'azure-active-directory-access-token' || authentication.type === 'azure-active-directory-msi-vm' || authentication.type === 'azure-active-directory-msi-app-service') {
19241970
if (token.fedAuth === undefined) {
19251971
this.loginError = ConnectionError('Did not receive Active Directory authentication acknowledgement');
19261972
this.loggedIn = false;
@@ -2025,19 +2071,49 @@ Connection.prototype.STATE = {
20252071
},
20262072
message: function() {
20272073
if (this.fedAuthInfoToken && this.fedAuthInfoToken.stsurl && this.fedAuthInfoToken.spn) {
2028-
const clientId = '7f98cb04-cd1e-40df-9140-3bf7e2cea4db';
2029-
const context = new AuthenticationContext(this.fedAuthInfoToken.stsurl);
2030-
const authentication = this.config.authentication;
2074+
const { authentication } = this.config;
2075+
2076+
const getToken = (callback) => {
2077+
const getTokenFromCredentials = (err, credentials) => {
2078+
if (err) {
2079+
return callback(err);
2080+
}
2081+
2082+
credentials.getToken().then((tokenResponse) => {
2083+
callback(null, tokenResponse.accessToken);
2084+
}, callback);
2085+
};
2086+
2087+
if (authentication.type === 'azure-active-directory-password') {
2088+
loginWithUsernamePassword(authentication.options.userName, authentication.options.password, {
2089+
clientId: '7f98cb04-cd1e-40df-9140-3bf7e2cea4db',
2090+
tokenAudience: this.fedAuthInfoToken.spn
2091+
}, getTokenFromCredentials);
2092+
} else if (authentication.type === 'azure-active-directory-msi-vm') {
2093+
loginWithVmMSI({
2094+
clientId: authentication.options.clientId,
2095+
msiEndpoint: authentication.options.msiEndpoint,
2096+
resource: this.fedAuthInfoToken.spn
2097+
}, getTokenFromCredentials);
2098+
} else if (authentication.type === 'azure-active-directory-msi-app-service') {
2099+
loginWithAppServiceMSI({
2100+
clientId: authentication.options.clientId,
2101+
msiEndpoint: authentication.options.msiEndpoint,
2102+
msiSecret: authentication.options.msiSecret,
2103+
resource: this.fedAuthInfoToken.spn
2104+
}, getTokenFromCredentials);
2105+
}
2106+
};
20312107

2032-
context.acquireTokenWithUsernamePassword(this.fedAuthInfoToken.spn, authentication.options.userName, authentication.options.password, clientId, (err, tokenResponse) => {
2108+
getToken((err, token) => {
20332109
if (err) {
20342110
this.loginError = ConnectionError('Security token could not be authenticated or authorized.', 'EFEDAUTH');
20352111
this.emit('connect', this.loginError);
20362112
this.transitionTo(this.STATE.FINAL);
20372113
return;
20382114
}
20392115

2040-
this.sendFedAuthResponsePacket(tokenResponse);
2116+
this.sendFedAuthTokenMessage(token);
20412117
});
20422118
} else if (this.loginError) {
20432119
if (this.loginError.isTransient) {

0 commit comments

Comments
 (0)