diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 78bd477f7ad9..d6a25743abc7 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -1058,6 +1058,12 @@ packages: dev: false resolution: integrity: sha512-dIPoZ3g5gcx9zZEszaxLSVTvMReD3xxyyDnQUjA6IYDG9Ba2AV0otMPs+77sG9ojB4Qr2N2Vk5RnKeuA0X/0bg== + /@types/stoppable/1.1.0: + dependencies: + '@types/node': 10.17.51 + dev: false + resolution: + integrity: sha512-BRR23Q9CJduH7AM6mk4JRttd8XyFkb4qIPZu4mdLF+VoP+wcjIxIWIKiBbN78NBbEuynrAyMPtzOHnIp2B/JPQ== /@types/tough-cookie/4.0.0: dev: false resolution: @@ -6464,6 +6470,13 @@ packages: node: '>= 0.6' resolution: integrity: sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + /stoppable/1.1.0: + dev: false + engines: + node: '>=4' + npm: '>=6' + resolution: + integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw== /stream-browserify/2.0.2: dependencies: inherits: 2.0.4 @@ -7296,6 +7309,7 @@ packages: resolution: integrity: sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= /xmldom/0.4.0: + deprecated: Deprecated due to CVE-2021-21366 resolved in 0.5.0 dev: false engines: node: '>=10.0.0' @@ -7828,7 +7842,7 @@ packages: dev: false name: '@rush-temp/communication-administration' resolution: - integrity: sha512-UprsjVTks6zBRv3FMc94LDoqXtYJqHsF2lMhuwOSRQXbfsuhS4MujCJc8zgraww/XoEJ4mk766EiIzKWwCFt1Q== + integrity: sha512-htkjuObGhUFw5iWZa6L+rGOjhBcin/huN6mdNlXxCiu8275OjYP1Ok8vYgvGQpshpZc9xQQV2ukLbI6g0/j5dQ== tarball: file:projects/communication-administration.tgz version: 0.0.0 file:projects/communication-chat.tgz: @@ -7884,7 +7898,7 @@ packages: dev: false name: '@rush-temp/communication-chat' resolution: - integrity: sha512-i3KOyWl2aDfSk/1RL4KZIf+ZMF5GUxt3dKgGVFrrUqu8E8Tub7+8vuXpfO5FAdzBaUvo7Q7eR58LYfXANsDSYQ== + integrity: sha512-epQP4pc9pGyaF23IMYVvBBjegtY6O+rfooIWlh9+WXpBRqMq7VSC8VJ1W2ZlEMmW3KJGo9MpXkIPxpfkRnXTjQ== tarball: file:projects/communication-chat.tgz version: 0.0.0 file:projects/communication-common.tgz: @@ -7993,7 +8007,7 @@ packages: dev: false name: '@rush-temp/communication-identity' resolution: - integrity: sha512-ecBIZ1BSe9aZrFKKBvocHF/j1rLUd8IN3zyDg2cWsZjXEvOXN3Rv3wT19URMP7AmthcwUo/iKoARp0szyP0A9Q== + integrity: sha512-j+ZoDVWhyvN3FoVITUTTtrWEQN1CxtfgZyRhf93Ax6JnAZIkKldk3ErkAf2BO4FPCSciKNCR/u/B2rV0DSlipg== tarball: file:projects/communication-identity.tgz version: 0.0.0 file:projects/communication-sms.tgz: @@ -8046,7 +8060,7 @@ packages: dev: false name: '@rush-temp/communication-sms' resolution: - integrity: sha512-OJopq1e/zTEsv4y9tCVZyngUtMbAmV7WseGCfWCkm//lI8aj+zxiozxyL4lQD0RMNMVaqi/N88hDGn0ANQvzRA== + integrity: sha512-yJiFHY9RKZ0bXQtLZroIpkhCua/a42br0ARIiBuKdMgTEo0fC0ICn0vsmMolS62ox9eyRXmvns9OBa9CGiN+sA== tarball: file:projects/communication-sms.tgz version: 0.0.0 file:projects/core-amqp.tgz: @@ -9016,6 +9030,7 @@ packages: '@types/node': 8.10.66 '@types/qs': 6.9.5 '@types/sinon': 9.0.10 + '@types/stoppable': 1.1.0 '@types/uuid': 8.3.0 assert: 1.5.0 axios: 0.21.1 @@ -9047,6 +9062,7 @@ packages: rollup-plugin-terser: 5.3.1_rollup@1.32.1 rollup-plugin-visualizer: 4.2.0_rollup@1.32.1 sinon: 9.2.4 + stoppable: 1.1.0 tslib: 2.1.0 typedoc: 0.15.2 typescript: 4.1.2 @@ -9057,7 +9073,7 @@ packages: optionalDependencies: keytar: 7.3.0 resolution: - integrity: sha512-RkgNLuiYgllsKZCWqAgG5YWRjgD/90RbafjKLQa/t0BYJLbvR3k/c9UVNGZDSh6ScpnSCmQ8eCr0+FGM3eB2yg== + integrity: sha512-Iqgb4vcT5ioFu7BGNFqdz5dAXkSDYZwb9BtLGLQkDRLv0nzpgCrNDnHJ6dc0ForlEsRk9ld0yBnpLP7ahiZZ8A== tarball: file:projects/identity.tgz version: 0.0.0 file:projects/keyvault-admin.tgz: diff --git a/sdk/identity/identity/CHANGELOG.md b/sdk/identity/identity/CHANGELOG.md index f37b92839ad5..602798883764 100644 --- a/sdk/identity/identity/CHANGELOG.md +++ b/sdk/identity/identity/CHANGELOG.md @@ -1,5 +1,9 @@ # Release History +## 1.2.5 (2021-03-25) + +- For the `InteractiveBrowserCredential` for Node.js, we've replaced the use of the `express` module with a native HTTP server, shrinking the resulting `@azure/identity` module considerably. + ## 1.2.4 (2021-03-08) - Bug fix: Now if the `managedIdentityClientId` optional parameter is provided to `DefaultAzureCredential`, it will be properly passed through to the underlying `ManagedIdentityCredential`. Related to customer issue: [13872](https://github.com/Azure/azure-sdk-for-js/issues/13872). diff --git a/sdk/identity/identity/package.json b/sdk/identity/identity/package.json index f82fe1a31f09..94d0192513a7 100644 --- a/sdk/identity/identity/package.json +++ b/sdk/identity/identity/package.json @@ -1,7 +1,7 @@ { "name": "@azure/identity", "sdk-type": "client", - "version": "1.2.4", + "version": "1.2.5", "description": "Provides credential implementations for Azure SDK libraries that can authenticate with Azure Active Directory", "main": "dist/index.js", "module": "dist-esm/src/index.js", @@ -85,12 +85,14 @@ "@azure/logger": "^1.0.0", "@azure/msal-node": "1.0.0-beta.6", "@opentelemetry/api": "^0.10.2", + "@types/stoppable": "^1.1.0", "axios": "^0.21.1", "events": "^3.0.0", "jws": "^4.0.0", "msal": "^1.0.2", "open": "^7.0.0", "qs": "^6.7.0", + "stoppable": "^1.1.0", "tslib": "^2.0.0", "uuid": "^8.3.0" }, @@ -107,7 +109,6 @@ "@rollup/plugin-multi-entry": "^3.0.0", "@rollup/plugin-node-resolve": "^8.0.0", "@rollup/plugin-replace": "^2.2.0", - "@types/express": "^4.16.0", "@types/jws": "^3.2.2", "@types/mocha": "^7.0.2", "@types/node": "^8.0.0", @@ -116,7 +117,6 @@ "assert": "^1.4.1", "cross-env": "^7.0.2", "eslint": "^7.15.0", - "express": "^4.16.3", "inherits": "^2.0.3", "karma": "^5.1.0", "karma-chrome-launcher": "^3.0.0", diff --git a/sdk/identity/identity/src/credentials/interactiveBrowserCredential.ts b/sdk/identity/identity/src/credentials/interactiveBrowserCredential.ts index 93576fd19bc1..4237c0e35812 100644 --- a/sdk/identity/identity/src/credentials/interactiveBrowserCredential.ts +++ b/sdk/identity/identity/src/credentials/interactiveBrowserCredential.ts @@ -11,9 +11,10 @@ import { Socket } from "net"; import { AuthenticationRequired, MsalClient } from "../client/msalClient"; import { AuthorizationCodeRequest } from "@azure/msal-node"; -import express from "express"; import open from "open"; import http from "http"; +import stoppable from "stoppable"; + import { checkTenantId } from "../util/checkTenantId"; const logger = credentialLogger("InteractiveBrowserCredential"); @@ -26,6 +27,7 @@ const logger = credentialLogger("InteractiveBrowserCredential"); export class InteractiveBrowserCredential implements TokenCredential { private redirectUri: string; private port: number; + private hostname: string; private msalClient: MsalClient; constructor(options?: InteractiveBrowserCredentialOptions) { @@ -53,6 +55,8 @@ export class InteractiveBrowserCredential implements TokenCredential { this.port = 80; } + this.hostname = url.hostname; + let authorityHost; if (options && options.authorityHost) { if (options.authorityHost.endsWith("/")) { @@ -112,74 +116,111 @@ export class InteractiveBrowserCredential implements TokenCredential { await open(response); } - private async acquireTokenFromBrowser(scopeArray: string[]): Promise { - // eslint-disable-next-line - return new Promise(async (resolve, reject) => { - // eslint-disable-next-line - let listen: http.Server | undefined; - let socketToDestroy: Socket | undefined; + private acquireTokenFromBrowser(scopeArray: string[]): Promise { + return new Promise((resolve, reject) => { + const socketToDestroy: Socket[] = []; - function cleanup(): void { - if (listen) { - listen.close(); + const requestListener = (req: http.IncomingMessage, res: http.ServerResponse) => { + if (!req.url) { + reject( + new Error( + `Interactive Browser Authentication Error "Did not receive token with a valid expiration"` + ) + ); + return; } - if (socketToDestroy) { - socketToDestroy.destroy(); + let url: URL; + try { + url = new URL(req.url, this.redirectUri); + } catch (e) { + reject( + new Error( + `Interactive Browser Authentication Error "Did not receive token with a valid expiration"` + ) + ); + return; } - } - - // Create Express App and Routes - const app = express(); - - app.get("/", async (req, res) => { const tokenRequest: AuthorizationCodeRequest = { - code: req.query.code as string, + code: url.searchParams.get("code")!, redirectUri: this.redirectUri, scopes: scopeArray }; - try { - const authResponse = await this.msalClient.acquireTokenByCode(tokenRequest); - const successMessage = `Authentication Complete. You can close the browser and return to the application.`; - if (authResponse && authResponse.expiresOn) { - const expiresOnTimestamp = authResponse?.expiresOn.valueOf(); - res.status(200).send(successMessage); - logger.getToken.info(formatSuccess(scopeArray)); - - resolve({ - expiresOnTimestamp, - token: authResponse.accessToken - }); - } else { + this.msalClient + .acquireTokenByCode(tokenRequest) + .then((authResponse) => { + const successMessage = `Authentication Complete. You can close the browser and return to the application.`; + if (authResponse && authResponse.expiresOn) { + const expiresOnTimestamp = authResponse?.expiresOn.valueOf(); + res.writeHead(200); + res.end(successMessage); + logger.getToken.info(formatSuccess(scopeArray)); + + resolve({ + expiresOnTimestamp, + token: authResponse.accessToken + }); + } else { + const errorMessage = formatError( + scopeArray, + `${url.searchParams.get("error")}. ${url.searchParams.get("error_description")}` + ); + res.writeHead(500); + res.end(errorMessage); + logger.getToken.info(errorMessage); + + reject( + new Error( + `Interactive Browser Authentication Error "Did not receive token with a valid expiration"` + ) + ); + } + cleanup(); + return; + }) + .catch(() => { + const errorMessage = formatError( + scopeArray, + `${url.searchParams.get("error")}. ${url.searchParams.get("error_description")}` + ); + res.writeHead(500); + res.end(errorMessage); + logger.getToken.info(errorMessage); + reject( new Error( `Interactive Browser Authentication Error "Did not receive token with a valid expiration"` ) ); - } - } catch (error) { - const errorMessage = formatError( - scopeArray, - `${req.query["error"]}. ${req.query["error_description"]}` - ); - res.status(500).send(errorMessage); - logger.getToken.info(errorMessage); - reject(new Error(errorMessage)); - } finally { - cleanup(); - } - }); + cleanup(); + }); + }; + const app = http.createServer(requestListener); - listen = app.listen(this.port, () => - logger.info(`Msal Node Auth Code Sample app listening on port ${this.port}!`) + const listen = app.listen(this.port, this.hostname, () => + logger.info(`InteractiveBrowerCredential listening on port ${this.port}!`) ); - listen.on("connection", (socket) => (socketToDestroy = socket)); + app.on("connection", (socket) => socketToDestroy.push(socket)); + const server = stoppable(app); - try { - await this.openAuthCodeUrl(scopeArray); - } catch (e) { + this.openAuthCodeUrl(scopeArray).catch((e) => { cleanup(); - throw e; + reject(e); + }); + + function cleanup(): void { + if (listen) { + listen.close(); + } + + for (const socket of socketToDestroy) { + socket.destroy(); + } + + if (server) { + server.close(); + server.stop(); + } } }); }