Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/microsoft-authentication/src/node/authServer.ts
3320 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
import * as http from 'http';
6
import { URL } from 'url';
7
import * as fs from 'fs';
8
import * as path from 'path';
9
import { randomBytes } from 'crypto';
10
11
function sendFile(res: http.ServerResponse, filepath: string) {
12
fs.readFile(filepath, (err, body) => {
13
if (err) {
14
console.error(err);
15
res.writeHead(404);
16
res.end();
17
} else {
18
res.writeHead(200, {
19
'content-length': body.length,
20
});
21
res.end(body);
22
}
23
});
24
}
25
26
interface IOAuthResult {
27
code: string;
28
state: string;
29
}
30
31
interface ILoopbackServer {
32
/**
33
* If undefined, the server is not started yet.
34
*/
35
port: number | undefined;
36
37
/**
38
* The nonce used
39
*/
40
nonce: string;
41
42
/**
43
* The state parameter used in the OAuth flow.
44
*/
45
state: string | undefined;
46
47
/**
48
* Starts the server.
49
* @returns The port to listen on.
50
* @throws If the server fails to start.
51
* @throws If the server is already started.
52
*/
53
start(): Promise<number>;
54
/**
55
* Stops the server.
56
* @throws If the server is not started.
57
* @throws If the server fails to stop.
58
*/
59
stop(): Promise<void>;
60
/**
61
* Returns a promise that resolves to the result of the OAuth flow.
62
*/
63
waitForOAuthResponse(): Promise<IOAuthResult>;
64
}
65
66
export class LoopbackAuthServer implements ILoopbackServer {
67
private readonly _server: http.Server;
68
private readonly _resultPromise: Promise<IOAuthResult>;
69
private _startingRedirect: URL;
70
71
public nonce = randomBytes(16).toString('base64');
72
public port: number | undefined;
73
74
public set state(state: string | undefined) {
75
if (state) {
76
this._startingRedirect.searchParams.set('state', state);
77
} else {
78
this._startingRedirect.searchParams.delete('state');
79
}
80
}
81
public get state(): string | undefined {
82
return this._startingRedirect.searchParams.get('state') ?? undefined;
83
}
84
85
constructor(serveRoot: string, startingRedirect: string) {
86
if (!serveRoot) {
87
throw new Error('serveRoot must be defined');
88
}
89
if (!startingRedirect) {
90
throw new Error('startingRedirect must be defined');
91
}
92
this._startingRedirect = new URL(startingRedirect);
93
let deferred: { resolve: (result: IOAuthResult) => void; reject: (reason: any) => void };
94
this._resultPromise = new Promise<IOAuthResult>((resolve, reject) => deferred = { resolve, reject });
95
96
this._server = http.createServer((req, res) => {
97
const reqUrl = new URL(req.url!, `http://${req.headers.host}`);
98
switch (reqUrl.pathname) {
99
case '/signin': {
100
const receivedNonce = (reqUrl.searchParams.get('nonce') ?? '').replace(/ /g, '+');
101
if (receivedNonce !== this.nonce) {
102
res.writeHead(302, { location: `/?error=${encodeURIComponent('Nonce does not match.')}` });
103
res.end();
104
}
105
res.writeHead(302, { location: this._startingRedirect.toString() });
106
res.end();
107
break;
108
}
109
case '/callback': {
110
const code = reqUrl.searchParams.get('code') ?? undefined;
111
const state = reqUrl.searchParams.get('state') ?? undefined;
112
const nonce = (reqUrl.searchParams.get('nonce') ?? '').replace(/ /g, '+');
113
const error = reqUrl.searchParams.get('error') ?? undefined;
114
if (error) {
115
res.writeHead(302, { location: `/?error=${reqUrl.searchParams.get('error_description')}` });
116
res.end();
117
deferred.reject(new Error(error));
118
break;
119
}
120
if (!code || !state || !nonce) {
121
res.writeHead(400);
122
res.end();
123
break;
124
}
125
if (this.state !== state) {
126
res.writeHead(302, { location: `/?error=${encodeURIComponent('State does not match.')}` });
127
res.end();
128
deferred.reject(new Error('State does not match.'));
129
break;
130
}
131
if (this.nonce !== nonce) {
132
res.writeHead(302, { location: `/?error=${encodeURIComponent('Nonce does not match.')}` });
133
res.end();
134
deferred.reject(new Error('Nonce does not match.'));
135
break;
136
}
137
deferred.resolve({ code, state });
138
res.writeHead(302, { location: '/' });
139
res.end();
140
break;
141
}
142
// Serve the static files
143
case '/':
144
sendFile(res, path.join(serveRoot, 'index.html'));
145
break;
146
default:
147
// substring to get rid of leading '/'
148
sendFile(res, path.join(serveRoot, reqUrl.pathname.substring(1)));
149
break;
150
}
151
});
152
}
153
154
public start(): Promise<number> {
155
return new Promise<number>((resolve, reject) => {
156
if (this._server.listening) {
157
throw new Error('Server is already started');
158
}
159
const portTimeout = setTimeout(() => {
160
reject(new Error('Timeout waiting for port'));
161
}, 5000);
162
this._server.on('listening', () => {
163
const address = this._server.address();
164
if (typeof address === 'string') {
165
this.port = parseInt(address);
166
} else if (address instanceof Object) {
167
this.port = address.port;
168
} else {
169
throw new Error('Unable to determine port');
170
}
171
172
clearTimeout(portTimeout);
173
174
// set state which will be used to redirect back to vscode
175
this.state = `http://127.0.0.1:${this.port}/callback?nonce=${encodeURIComponent(this.nonce)}`;
176
177
resolve(this.port);
178
});
179
this._server.on('error', err => {
180
reject(new Error(`Error listening to server: ${err}`));
181
});
182
this._server.on('close', () => {
183
reject(new Error('Closed'));
184
});
185
this._server.listen(0, '127.0.0.1');
186
});
187
}
188
189
public stop(): Promise<void> {
190
return new Promise<void>((resolve, reject) => {
191
if (!this._server.listening) {
192
throw new Error('Server is not started');
193
}
194
this._server.close((err) => {
195
if (err) {
196
reject(err);
197
} else {
198
resolve();
199
}
200
});
201
});
202
}
203
204
public waitForOAuthResponse(): Promise<IOAuthResult> {
205
return this._resultPromise;
206
}
207
}
208
209