wangzhibo
2026-05-09 53418655c061ac26f66058253f39200aced1ab1d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
/*
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License.
 */
 
import {
    AuthorizeResponse,
    Constants as CommonConstants,
    UrlUtils,
} from "@azure/msal-common/node";
import http from "http";
import { NodeAuthError } from "../error/NodeAuthError.js";
import { Constants } from "../utils/Constants.js";
import { ILoopbackClient } from "./ILoopbackClient.js";
 
export class LoopbackClient implements ILoopbackClient {
    private server: http.Server | undefined;
 
    /**
     * Spins up a loopback server which returns the server response when the localhost redirectUri is hit
     * @param successTemplate
     * @param errorTemplate
     * @returns
     */
    async listenForAuthCode(
        successTemplate?: string,
        errorTemplate?: string
    ): Promise<AuthorizeResponse> {
        if (this.server) {
            throw NodeAuthError.createLoopbackServerAlreadyExistsError();
        }
 
        return new Promise<AuthorizeResponse>((resolve, reject) => {
            this.server = http.createServer(
                (req: http.IncomingMessage, res: http.ServerResponse) => {
                    const url = req.url;
                    if (!url) {
                        res.end(
                            errorTemplate ||
                                "Error occurred loading redirectUrl"
                        );
                        reject(
                            NodeAuthError.createUnableToLoadRedirectUrlError()
                        );
                        return;
                    } else if (url === CommonConstants.FORWARD_SLASH) {
                        res.end(
                            successTemplate ||
                                "Auth code was successfully acquired. You can close this window now."
                        );
                        return;
                    }
 
                    const redirectUri = this.getRedirectUri();
                    const parsedUrl = new URL(url, redirectUri);
                    const authCodeResponse =
                        UrlUtils.getDeserializedResponse(parsedUrl.search) ||
                        {};
                    if (authCodeResponse.code) {
                        res.writeHead(CommonConstants.HTTP_REDIRECT, {
                            location: redirectUri,
                        }); // Prevent auth code from being saved in the browser history
                        res.end();
                    }
                    if (authCodeResponse.error) {
                        res.end(
                            errorTemplate ||
                                `Error occurred: ${authCodeResponse.error}`
                        );
                    }
                    resolve(authCodeResponse);
                }
            );
            this.server.listen(0, "127.0.0.1"); // Listen on any available port
        });
    }
 
    /**
     * Get the port that the loopback server is running on
     * @returns
     */
    getRedirectUri(): string {
        if (!this.server || !this.server.listening) {
            throw NodeAuthError.createNoLoopbackServerExistsError();
        }
 
        const address = this.server.address();
        if (!address || typeof address === "string" || !address.port) {
            this.closeServer();
            throw NodeAuthError.createInvalidLoopbackAddressTypeError();
        }
 
        const port = address && address.port;
 
        return `${Constants.HTTP_PROTOCOL}${Constants.LOCALHOST}:${port}`;
    }
 
    /**
     * Close the loopback server
     */
    closeServer(): void {
        if (this.server) {
            // Only stops accepting new connections, server will close once open/idle connections are closed.
            this.server.close();
 
            if (typeof this.server.closeAllConnections === "function") {
                /*
                 * Close open/idle connections. This API is available in Node versions 18.2 and higher
                 */
                this.server.closeAllConnections();
            }
            this.server.unref();
            this.server = undefined;
        }
    }
}