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