Skip to content

Commit 5ab0b88

Browse files
author
Jonathan Turner
authored
[Identity] Remove express (Azure#13800)
* Remove use of express in InteractiveBrowserCredential * Remove use of express in InteractiveBrowserCredential * Fix lint issues * Use hostname instead of host * Add changelog entry * Address feedback * Address feedback * lint * address feedback * address feedback * lint
1 parent 0a24c62 commit 5ab0b88

File tree

4 files changed

+111
-54
lines changed

4 files changed

+111
-54
lines changed

common/config/rush/pnpm-lock.yaml

Lines changed: 16 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sdk/identity/identity/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
## 1.2.4-beta.2 (Unreleased)
66

7+
- Replaced the use of the 'express' module with a Node-native http server, shrinking the resulting identity module considerably
78
- `DefaultAzureCredential`'s implementation for browsers was simplified to throw a simple error instead of trying credentials that were already not supported for the browser.
89
- Breaking Change: `InteractiveBrowserCredential` for the browser now requires the client ID to be provided.
910
- Documentation was added to elaborate on how to configure an AAD application to support `InteractiveBrowserCredential`.

sdk/identity/identity/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,14 @@
8686
"@azure/msal-node": "1.0.0-beta.6",
8787
"@azure/msal-browser": "2.9.0",
8888
"@opentelemetry/api": "^0.10.2",
89-
"@types/express": "^4.16.0",
89+
"@types/stoppable": "^1.1.0",
9090
"axios": "^0.21.1",
9191
"events": "^3.0.0",
92-
"express": "^4.16.3",
9392
"jws": "^4.0.0",
9493
"msal": "^1.0.2",
9594
"open": "^7.0.0",
9695
"qs": "^6.7.0",
96+
"stoppable": "^1.1.0",
9797
"tslib": "^2.0.0",
9898
"uuid": "^8.3.0"
9999
},

sdk/identity/identity/src/credentials/interactiveBrowserCredential.ts

Lines changed: 92 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import { Socket } from "net";
1111
import { AuthenticationRequired, MsalClient } from "../client/msalClient";
1212
import { AuthorizationCodeRequest } from "@azure/msal-node";
1313

14-
import express from "express";
1514
import open from "open";
1615
import http from "http";
16+
import stoppable from "stoppable";
17+
1718
import { checkTenantId } from "../util/checkTenantId";
1819

1920
const logger = credentialLogger("InteractiveBrowserCredential");
@@ -26,6 +27,7 @@ const logger = credentialLogger("InteractiveBrowserCredential");
2627
export class InteractiveBrowserCredential implements TokenCredential {
2728
private redirectUri: string;
2829
private port: number;
30+
private hostname: string;
2931
private msalClient: MsalClient;
3032

3133
constructor(options: InteractiveBrowserCredentialOptions = {}) {
@@ -53,6 +55,8 @@ export class InteractiveBrowserCredential implements TokenCredential {
5355
this.port = 80;
5456
}
5557

58+
this.hostname = url.hostname;
59+
5660
let authorityHost;
5761
if (options.authorityHost) {
5862
if (options.authorityHost.endsWith("/")) {
@@ -112,74 +116,111 @@ export class InteractiveBrowserCredential implements TokenCredential {
112116
await open(response);
113117
}
114118

115-
private async acquireTokenFromBrowser(scopeArray: string[]): Promise<AccessToken | null> {
116-
// eslint-disable-next-line
117-
return new Promise<AccessToken | null>(async (resolve, reject) => {
118-
// eslint-disable-next-line
119-
let listen: http.Server | undefined;
120-
let socketToDestroy: Socket | undefined;
119+
private acquireTokenFromBrowser(scopeArray: string[]): Promise<AccessToken | null> {
120+
return new Promise<AccessToken | null>((resolve, reject) => {
121+
const socketToDestroy: Socket[] = [];
121122

122-
function cleanup(): void {
123-
if (listen) {
124-
listen.close();
123+
const requestListener = (req: http.IncomingMessage, res: http.ServerResponse) => {
124+
if (!req.url) {
125+
reject(
126+
new Error(
127+
`Interactive Browser Authentication Error "Did not receive token with a valid expiration"`
128+
)
129+
);
130+
return;
125131
}
126-
if (socketToDestroy) {
127-
socketToDestroy.destroy();
132+
let url: URL;
133+
try {
134+
url = new URL(req.url, this.redirectUri);
135+
} catch (e) {
136+
reject(
137+
new Error(
138+
`Interactive Browser Authentication Error "Did not receive token with a valid expiration"`
139+
)
140+
);
141+
return;
128142
}
129-
}
130-
131-
// Create Express App and Routes
132-
const app = express();
133-
134-
app.get("/", async (req, res) => {
135143
const tokenRequest: AuthorizationCodeRequest = {
136-
code: req.query.code as string,
144+
code: url.searchParams.get("code")!,
137145
redirectUri: this.redirectUri,
138146
scopes: scopeArray
139147
};
140148

141-
try {
142-
const authResponse = await this.msalClient.acquireTokenByCode(tokenRequest);
143-
const successMessage = `Authentication Complete. You can close the browser and return to the application.`;
144-
if (authResponse && authResponse.expiresOn) {
145-
const expiresOnTimestamp = authResponse?.expiresOn.valueOf();
146-
res.status(200).send(successMessage);
147-
logger.getToken.info(formatSuccess(scopeArray));
148-
149-
resolve({
150-
expiresOnTimestamp,
151-
token: authResponse.accessToken
152-
});
153-
} else {
149+
this.msalClient
150+
.acquireTokenByCode(tokenRequest)
151+
.then((authResponse) => {
152+
const successMessage = `Authentication Complete. You can close the browser and return to the application.`;
153+
if (authResponse && authResponse.expiresOn) {
154+
const expiresOnTimestamp = authResponse?.expiresOn.valueOf();
155+
res.writeHead(200);
156+
res.end(successMessage);
157+
logger.getToken.info(formatSuccess(scopeArray));
158+
159+
resolve({
160+
expiresOnTimestamp,
161+
token: authResponse.accessToken
162+
});
163+
} else {
164+
const errorMessage = formatError(
165+
scopeArray,
166+
`${url.searchParams.get("error")}. ${url.searchParams.get("error_description")}`
167+
);
168+
res.writeHead(500);
169+
res.end(errorMessage);
170+
logger.getToken.info(errorMessage);
171+
172+
reject(
173+
new Error(
174+
`Interactive Browser Authentication Error "Did not receive token with a valid expiration"`
175+
)
176+
);
177+
}
178+
cleanup();
179+
return;
180+
})
181+
.catch(() => {
182+
const errorMessage = formatError(
183+
scopeArray,
184+
`${url.searchParams.get("error")}. ${url.searchParams.get("error_description")}`
185+
);
186+
res.writeHead(500);
187+
res.end(errorMessage);
188+
logger.getToken.info(errorMessage);
189+
154190
reject(
155191
new Error(
156192
`Interactive Browser Authentication Error "Did not receive token with a valid expiration"`
157193
)
158194
);
159-
}
160-
} catch (error) {
161-
const errorMessage = formatError(
162-
scopeArray,
163-
`${req.query["error"]}. ${req.query["error_description"]}`
164-
);
165-
res.status(500).send(errorMessage);
166-
logger.getToken.info(errorMessage);
167-
reject(new Error(errorMessage));
168-
} finally {
169-
cleanup();
170-
}
171-
});
195+
cleanup();
196+
});
197+
};
198+
const app = http.createServer(requestListener);
172199

173-
listen = app.listen(this.port, () =>
200+
const listen = app.listen(this.port, this.hostname, () =>
174201
logger.info(`InteractiveBrowerCredential listening on port ${this.port}!`)
175202
);
176-
listen.on("connection", (socket) => (socketToDestroy = socket));
203+
app.on("connection", (socket) => socketToDestroy.push(socket));
204+
const server = stoppable(app);
177205

178-
try {
179-
await this.openAuthCodeUrl(scopeArray);
180-
} catch (e) {
206+
this.openAuthCodeUrl(scopeArray).catch((e) => {
181207
cleanup();
182-
throw e;
208+
reject(e);
209+
});
210+
211+
function cleanup(): void {
212+
if (listen) {
213+
listen.close();
214+
}
215+
216+
for (const socket of socketToDestroy) {
217+
socket.destroy();
218+
}
219+
220+
if (server) {
221+
server.close();
222+
server.stop();
223+
}
183224
}
184225
});
185226
}

0 commit comments

Comments
 (0)