diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index b8526473bff5..14a630d39a59 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -1186,6 +1186,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: @@ -7351,6 +7357,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 @@ -9993,6 +10006,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 @@ -10024,6 +10038,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 @@ -10034,7 +10049,7 @@ packages: optionalDependencies: keytar: 7.3.0 resolution: - integrity: sha512-tFR61LCHIZDfYb97aYITRuRpTDM7+7O4bQt+Rxuj07YPyrHQH11lmrFsSq2RwaEJUG6W9qfAkvo2ONiPK5k3fw== + integrity: sha512-n9Sn03tFbNDFSt+Z5Ofvd6weyUrUxgVV0KiCfg6ztX6NlRk7dRPHE7oW0zFs5dpYzF0oVqANkrCL+8j3Z3l5dA== 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 3de3e50242e2..24381431b02c 100644 --- a/sdk/identity/identity/CHANGELOG.md +++ b/sdk/identity/identity/CHANGELOG.md @@ -4,6 +4,7 @@ ## 1.2.4-beta.2 (Unreleased) +- Replaced the use of the 'express' module with a Node-native http server, shrinking the resulting identity module considerably - `DefaultAzureCredential`'s implementation for browsers was simplified to throw a simple error instead of trying credentials that were already not supported for the browser. - Breaking Change: `InteractiveBrowserCredential` for the browser now requires the client ID to be provided. - Documentation was added to elaborate on how to configure an AAD application to support `InteractiveBrowserCredential`. diff --git a/sdk/identity/identity/package.json b/sdk/identity/identity/package.json index 9b21af3677c2..baedc591e207 100644 --- a/sdk/identity/identity/package.json +++ b/sdk/identity/identity/package.json @@ -86,14 +86,14 @@ "@azure/msal-node": "1.0.0-beta.6", "@azure/msal-browser": "2.9.0", "@opentelemetry/api": "^0.10.2", - "@types/express": "^4.16.0", + "@types/stoppable": "^1.1.0", "axios": "^0.21.1", "events": "^3.0.0", - "express": "^4.16.3", "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" }, diff --git a/sdk/identity/identity/src/credentials/interactiveBrowserCredential.ts b/sdk/identity/identity/src/credentials/interactiveBrowserCredential.ts index d0e47cf6bfe9..adf5443c99b8 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.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, () => + 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(); + } } }); }