Skip to content

Commit 1edab37

Browse files
committed
update electron sample to make use of ILoopbackClient
1 parent 13df73a commit 1edab37

File tree

3 files changed

+152
-2
lines changed

3 files changed

+152
-2
lines changed

samples/msal-node-samples/ElectronSystemBrowserTestApp/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"start": "electron-forge start",
1010
"package": "electron-forge package",
1111
"build:package": "cd ../../../lib/msal-common && npm run build && cd ../msal-node && npm run build",
12+
"install:local": "npm install ../../../lib/msal-common && npm install ../../../lib/msal-node",
1213
"make": "electron-forge make",
1314
"publish": "electron-forge publish",
1415
"lint": "eslint --ext .ts,.tsx ."
@@ -47,7 +48,8 @@
4748
"typescript": "~4.5.4"
4849
},
4950
"dependencies": {
50-
"@azure/msal-node": "^1.14.2",
51+
"@azure/msal-common": "file:../../../lib/msal-common",
52+
"@azure/msal-node": "file:../../../lib/msal-node",
5153
"axios": "^1.1.3",
5254
"bootstrap": "^5.2.2",
5355
"electron-squirrel-startup": "^1.0.0"

samples/msal-node-samples/ElectronSystemBrowserTestApp/src/AuthProvider.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
InteractiveRequest,
88
SilentFlowRequest,
99
} from "@azure/msal-node";
10+
import { CustomLoopbackClient } from "./CustomLoopbackClient";
1011
import { cachePlugin } from "./CachePlugin";
1112
import * as fs from "fs";
1213

@@ -48,7 +49,7 @@ export default class AuthProvider {
4849
try {
4950
if (!this.account) {
5051
return;
51-
}
52+
}
5253
await this.clientApplication
5354
.getTokenCache()
5455
.removeAccount(this.account);
@@ -111,6 +112,10 @@ export default class AuthProvider {
111112
tokenRequest: SilentFlowRequest
112113
): Promise<AuthenticationResult> {
113114
try {
115+
// a custom loopback server which can attempt to listen on a preferred port
116+
const customLoopbackClient = await CustomLoopbackClient.initialize(3874);
117+
118+
// opens a browser instance via Electron shell API
114119
const openBrowser = async (url: any) => {
115120
await shell.openExternal(url);
116121
};
@@ -124,6 +129,7 @@ export default class AuthProvider {
124129
errorTemplate: fs
125130
.readFileSync("./public/errorTemplate.html", "utf8")
126131
.toString(),
132+
loopbackClient: customLoopbackClient // overrides default loopback client
127133
};
128134

129135
const authResponse =
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License.
4+
*/
5+
6+
import { createServer, IncomingMessage, Server, ServerResponse } from "http";
7+
import { Constants as CommonConstants, UrlString, ServerAuthorizationCodeResponse } from "@azure/msal-common";
8+
import { ILoopbackClient } from "@azure/msal-node";
9+
10+
/**
11+
* Implements ILoopbackClient interface to listen for authZ code response.
12+
* This custom implementation checks for a preferred port and uses it if available.
13+
*/
14+
export class CustomLoopbackClient implements ILoopbackClient {
15+
port: number = 0; // default port, which will be set to a random available port
16+
private server: Server;
17+
18+
private constructor(port: number = 0) {
19+
this.port = port;
20+
}
21+
22+
/**
23+
* Initializes a loopback server with an available port
24+
* @param preferredPort
25+
* @param logger
26+
* @returns
27+
*/
28+
static async initialize(preferredPort: number | undefined): Promise<CustomLoopbackClient> {
29+
const loopbackClient = new CustomLoopbackClient();
30+
31+
if (preferredPort === 0 || preferredPort === undefined) {
32+
return loopbackClient;
33+
}
34+
const isPortAvailable = await loopbackClient.isPortAvailable(preferredPort);
35+
36+
if (isPortAvailable) {
37+
loopbackClient.port = preferredPort;
38+
}
39+
40+
return loopbackClient;
41+
}
42+
43+
/**
44+
* Spins up a loopback server which returns the server response when the localhost redirectUri is hit
45+
* @param successTemplate
46+
* @param errorTemplate
47+
* @returns
48+
*/
49+
async listenForAuthCode(successTemplate?: string, errorTemplate?: string): Promise<ServerAuthorizationCodeResponse> {
50+
if (!!this.server) {
51+
throw new Error('Loopback server already exists. Cannot create another.')
52+
}
53+
54+
const authCodeListener = new Promise<ServerAuthorizationCodeResponse>((resolve, reject) => {
55+
this.server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
56+
const url = req.url;
57+
if (!url) {
58+
res.end(errorTemplate || "Error occurred loading redirectUrl");
59+
reject(new Error('Loopback server callback was invoked without a url. This is unexpected.'));
60+
return;
61+
} else if (url === CommonConstants.FORWARD_SLASH) {
62+
res.end(successTemplate || "Auth code was successfully acquired. You can close this window now.");
63+
return;
64+
}
65+
66+
const authCodeResponse = UrlString.getDeserializedQueryString(url);
67+
if (authCodeResponse.code) {
68+
const redirectUri = await this.getRedirectUri();
69+
res.writeHead(302, { location: redirectUri }); // Prevent auth code from being saved in the browser history
70+
res.end();
71+
}
72+
resolve(authCodeResponse);
73+
});
74+
this.server.listen(this.port);
75+
});
76+
77+
// Wait for server to be listening
78+
await new Promise<void>((resolve) => {
79+
let ticks = 0;
80+
const id = setInterval(() => {
81+
if ((5000 / 100) < ticks) {
82+
throw new Error('Timed out waiting for auth code listener to be registered.');
83+
}
84+
85+
if (this.server.listening) {
86+
clearInterval(id);
87+
resolve();
88+
}
89+
ticks++;
90+
}, 100);
91+
});
92+
93+
return authCodeListener;
94+
}
95+
96+
/**
97+
* Get the port that the loopback server is running on
98+
* @returns
99+
*/
100+
getRedirectUri(): string {
101+
if (!this.server) {
102+
throw new Error('No loopback server exists yet.')
103+
}
104+
105+
const address = this.server.address();
106+
if (!address || typeof address === "string" || !address.port) {
107+
this.closeServer();
108+
throw new Error('Loopback server address is not type string. This is unexpected.')
109+
}
110+
111+
const port = address && address.port;
112+
113+
return `http://localhost:${port}`;
114+
}
115+
116+
/**
117+
* Close the loopback server
118+
*/
119+
closeServer(): void {
120+
if (!!this.server) {
121+
this.server.close();
122+
}
123+
}
124+
125+
/**
126+
* Attempts to create a server and listen on a given port
127+
* @param port
128+
* @returns
129+
*/
130+
isPortAvailable(port: number): Promise<boolean> {
131+
return new Promise(resolve => {
132+
const server = createServer()
133+
.listen(port, () => {
134+
server.close();
135+
resolve(true);
136+
})
137+
.on("error", () => {
138+
resolve(false);
139+
});
140+
});
141+
}
142+
}

0 commit comments

Comments
 (0)