Mini Shell
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.OAuthProvider = void 0;
/*
Copyright 2022 The Sigstore Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const assert_1 = __importDefault(require("assert"));
const child_process_1 = __importDefault(require("child_process"));
const http_1 = __importDefault(require("http"));
const make_fetch_happen_1 = __importDefault(require("make-fetch-happen"));
const url_1 = require("url");
const util_1 = require("../util");
class OAuthProvider {
constructor(options) {
this.clientID = options.clientID;
this.clientSecret = options.clientSecret || '';
this.issuer = options.issuer;
this.redirectURI = options.redirectURL;
this.codeVerifier = generateRandomString(32);
this.state = generateRandomString(16);
}
async getToken() {
const authCode = await this.initiateAuthRequest();
return this.getIDToken(authCode);
}
// Initates the authorization request. This will start an HTTP server to
// receive the post-auth redirect and then open the user's default browser to
// the provider's authorization page.
async initiateAuthRequest() {
const server = http_1.default.createServer();
const sockets = new Set();
// Start server and wait till it is listening. If a redirect URL was
// provided, use that. Otherwise, use a random port and construct the
// redirect URL.
await new Promise((resolve) => {
if (this.redirectURI) {
const url = new url_1.URL(this.redirectURI);
server.listen(Number(url.port), url.hostname, resolve);
}
else {
server.listen(0, resolve);
// Get port the server is listening on and construct the server URL
const port = server.address().port;
this.redirectURI = `http://localhost:${port}`;
}
});
// Keep track of connections to the server so we can force a shutdown
server.on('connection', (socket) => {
sockets.add(socket);
socket.once('close', () => {
sockets.delete(socket);
});
});
const result = new Promise((resolve, reject) => {
// Set-up handler for post-auth redirect
server.on('request', (req, res) => {
if (!req.url) {
reject('invalid server request');
return;
}
res.writeHead(200);
res.end('Auth Successful');
// Parse incoming request URL
const query = new url_1.URL(req.url, this.redirectURI).searchParams;
// Check to see if the state matches
if (query.get('state') !== this.state) {
reject('invalid state value');
return;
}
const authCode = query.get('code');
// Force-close any open connections to the server so we can get a
// clean shutdown
for (const socket of sockets) {
socket.destroy();
sockets.delete(socket);
}
// Return auth code once we've shutdown server
server.close(() => {
if (!authCode) {
reject('authorization code not found');
}
else {
resolve(authCode);
}
});
});
});
try {
// Open browser to start authorization request
const authBaseURL = await this.issuer.authEndpoint();
const authURL = this.getAuthRequestURL(authBaseURL);
await this.openURL(authURL);
}
catch (err) {
// Prevent leaked server handler on error
server.close();
throw err;
}
return result;
}
// Uses the provided authorization code, to retrieve the ID token from the
// provider
async getIDToken(authCode) {
(0, assert_1.default)(this.redirectURI);
const tokenEndpointURL = await this.issuer.tokenEndpoint();
const params = new url_1.URLSearchParams();
params.append('grant_type', 'authorization_code');
params.append('code', authCode);
params.append('redirect_uri', this.redirectURI);
params.append('code_verifier', this.codeVerifier);
const response = await (0, make_fetch_happen_1.default)(tokenEndpointURL, {
method: 'POST',
headers: { Authorization: `Basic ${this.getBasicAuthHeaderValue()}` },
body: params,
}).then((r) => r.json());
return response.id_token;
}
// Construct the basic auth header value from the client ID and secret
getBasicAuthHeaderValue() {
return util_1.encoding.base64Encode(`${this.clientID}:${this.clientSecret}`);
}
// Generate starting URL for authorization request
getAuthRequestURL(baseURL) {
const params = this.getAuthRequestParams();
return `${baseURL}?${params.toString()}`;
}
// Collect parameters for authorization request
getAuthRequestParams() {
(0, assert_1.default)(this.redirectURI);
const codeChallenge = this.getCodeChallenge();
return new url_1.URLSearchParams({
response_type: 'code',
client_id: this.clientID,
client_secret: this.clientSecret,
scope: 'openid email',
redirect_uri: this.redirectURI,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
state: this.state,
nonce: generateRandomString(16),
});
}
// Generate code challenge for authorization request
getCodeChallenge() {
return util_1.encoding.base64URLEscape(util_1.crypto.hash(this.codeVerifier).toString('base64'));
}
// Open the supplied URL in the user's default browser
async openURL(url) {
return new Promise((resolve, reject) => {
let open = null;
let command = `"${url}"`;
switch (process.platform) {
case 'darwin':
open = 'open';
break;
case 'linux' || 'freebsd' || 'netbsd' || 'openbsd':
open = 'xdg-open';
break;
case 'win32':
open = 'start';
command = `"" ${command}`;
break;
default:
return reject(`OAuth: unsupported platform: ${process.platform}`);
}
console.error(`Your browser will now be opened to: ${url}`);
child_process_1.default.exec(`${open} ${command}`, undefined, (err) => {
if (err) {
reject(err);
}
else {
resolve();
}
});
});
}
}
exports.OAuthProvider = OAuthProvider;
// Generate random code verifier value
function generateRandomString(len) {
return util_1.encoding.base64URLEscape(util_1.crypto.randomBytes(len).toString('base64'));
}
Zerion Mini Shell 1.0