Skip to content

Commit

Permalink
updated schwab-authorize to work on Windows
Browse files Browse the repository at this point in the history
  • Loading branch information
slimandslam committed Nov 24, 2024
1 parent a47886d commit 0d124d6
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 101 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,10 @@ SCHWAB_SECRET=zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
```

2. Run `schwab-authorize` . After installing schwab-client-js, you should run the helper app named `schwab-authorize`. You should be able to run it from the command line when you're at the root of your project by typing `schwab-authorize`. It will construct a special Schwab login URL using your public key and try to open your desktop web browser at that URL. You will need to login with your schwab.com credentials (NOT your developer.schwab.com credentials). When you get to the final step, **your browser will likely give you a warning** because `schwab-authorize` is using a self-signed certificate to retrieve the returned https URL. Allow `schwab-authorize` to do its job and it will add the resulting SCHWAB_REFRESH_TOKEN to your .env file. <br />
▪ Note1: Schwab only lets you have one SCHWAB_REFRESH_TOKEN working per Schwab login. So, if you have multiple projects using schwab-client-js, you'll need to use the same SCHWAB_REFRESH_TOKEN for all of them. Creating a new SCHWAB_REFRESH_TOKEN invalidates the old ones. <br />
▪ Note2: `schwab-authorize` is written in Javascript. You can review the source at `node_modules/schwab-client-js/bin/schwab-authorize.js` . The self-signed cert is in the directory `node_modules/schwab-client-js/bin/sslcert` <br />
▪ Note1: On Windows, you may have to run it directly:
`C:\> node node_modules/schwab-client-js/bin/schwab-authorize.js` <br />
▪ Note2: Schwab only lets you have one SCHWAB_REFRESH_TOKEN working per Schwab login. So, if you have multiple projects using schwab-client-js, you'll need to use the same SCHWAB_REFRESH_TOKEN for all of them. Creating a new SCHWAB_REFRESH_TOKEN invalidates the old ones. <br />
▪ Note3: `schwab-authorize` is written in Javascript. You can review the source at `node_modules/schwab-client-js/bin/schwab-authorize.js` . The self-signed cert is in the directory `node_modules/schwab-client-js/bin/sslcert` <br />

3. Your `.env` file should now look like this (as previously mentioned, the SCHWAB_CALLBACK_URL is optional and will default to `https://127.0.0.1` if not provided):

Expand Down
189 changes: 90 additions & 99 deletions bin/schwab-authorize.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,146 +2,137 @@

// schwab-authorize.js
// Launches a web browser to create a new SCHWAB_REFRESH_TOKEN
// Requires your Schwab public and private keys which you should put in an .env file
// I have provided a self-signed SSL cert which is in the sslcert directory
//
// NOTE: the self-signed cert typically causes your web browser to issue a warning
//
// Place a SCHWAB_CALLBACK_URL in the .env file which must match what is defined
// in your app details on developer.schwab.com

import open from "open";
import dotenv from "dotenv";
import https from "https";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";

// Load environment variables
dotenv.config();

// Ensure required environment variables are set
if (!process.env.SCHWAB_APP_KEY) {
throw new Error("Environment variable SCHWAB_APP_KEY is not set.");
}

// Get the directory of this script
const __dirname = path.dirname(fileURLToPath(import.meta.url));

// Resolve paths to SSL certificate files
const sslDir = path.resolve(__dirname, "sslcert");
const keyPath = path.join(sslDir, "key.pem");
const certPath = path.join(sslDir, "cert.pem");

// Verify that SSL files exist
if (!fs.existsSync(keyPath) || !fs.existsSync(certPath)) {
throw new Error(
`Missing SSL certificate files in ${sslDir}. Ensure key.pem and cert.pem are present.`
);
}

// Load SSL options
const options = {
key: fs.readFileSync(keyPath, "utf-8"),
cert: fs.readFileSync(certPath, "utf-8"),
};

// Default callback URL
const callbackUrl = process.env.SCHWAB_CALLBACK_URL || "https://127.0.0.1";

// Construct authorization URL
const authorizationUrl =
`https://api.schwabapi.com/v1/oauth/authorize?client_id=${process.env.SCHWAB_APP_KEY}` +
`&redirect_uri=${callbackUrl}`;

// Extract port from the callback URL
const urlObj = new URL(callbackUrl);
const port = urlObj.port || (urlObj.protocol === "https:" ? "443" : "80");

// Function to update .env with new token
function updateEnv(newToken) {
const envFilePath = path.resolve(process.cwd(), ".env");

let envContent = "";
if (fs.existsSync(envFilePath)) {
envContent = fs.readFileSync(envFilePath, "utf8");
} else {
throw new Error("Expecting to find .env file. .env file not found");
}
let envContent = fs.existsSync(envFilePath)
? fs.readFileSync(envFilePath, "utf8")
: "";

if (envContent.includes("SCHWAB_REFRESH_TOKEN")) {
envContent = envContent.replace(
/SCHWAB_REFRESH_TOKEN=.*/,
`SCHWAB_REFRESH_TOKEN=${newToken}\n`,
`SCHWAB_REFRESH_TOKEN=${newToken}`
);
} else {
envContent += `\nSCHWAB_REFRESH_TOKEN=${newToken}\n`;
envContent += `\nSCHWAB_REFRESH_TOKEN=${newToken}`;
}

fs.writeFileSync(envFilePath, envContent, "utf8");

console.log("SCHWAB_REFRESH_TOKEN updated in .env file.");

process.exit(0);
}

// Function to get the authorization response
async function getAuthorizationResponse() {
return new Promise((resolve, reject) => {
// Create an HTTPS server to listen for the callback
const server = https.createServer(options, (req, res) => {
const fullUrl = `${process.env.SCHWAB_CALLBACK_URL}${req.url}`;
// console.log(`Received URL: ${fullUrl}`); // Print the full received URL

// Send a response back to the browser
const fullUrl = `${callbackUrl}${req.url}`;
res.writeHead(200, { "Content-Type": "text/html" });
res.end(
"<h1>Authorization successful!</h1><h2>You can close this window.</h2>",
);

// Close the server and resolve the full URL
res.end("<h1>Authorization successful! You can close this window.</h1>");
server.close();
resolve(fullUrl);
});

// Start the server on the specified port
server.listen(port, () => {
console.log("Listening for the HTTPS callback on ", callbackUrl);

// Open the authorization URL in the user's browser
console.log(`Listening for the HTTPS callback on ${callbackUrl}`);
open(authorizationUrl).catch((error) =>
reject(new Error(`Failed to open URL: ${error.message}`)),
reject(new Error(`Failed to open URL: ${error.message}`))
);
});

// Error handling for the server
server.on("error", (err) => {
reject(new Error(`Server error: ${err.message}`));
});
server.on("error", (err) => reject(new Error(`Server error: ${err.message}`)));
});
}

dotenv.config();

if (!process.env.SCHWAB_APP_KEY) {
throw new Error("Environment variable SCHWAB_APP_KEY is not set.");
}

// default to https://127.0.0.1 if it is not set
const callbackUrl = process.env.SCHWAB_CALLBACK_URL || "https://127.0.0.1";

const authorizationUrl =
"https://api.schwabapi.com/v1/oauth/authorize?client_id=" +
process.env.SCHWAB_APP_KEY +
"&redirect_uri=" +
callbackUrl;

const urlObj = new URL(callbackUrl);
const port = urlObj.port || (urlObj.protocol === "https:" ? "443" : "80");
// Main execution
(async () => {
try {
const receivedUrl = await getAuthorizationResponse();
console.log(`Received URL: ${receivedUrl}`);

// Get the directory of this script
const scriptDir = path.dirname(new URL(import.meta.url).pathname);

// Resolve the path to the SSL certificate
const keyPath = path.resolve(scriptDir, "./sslcert/key.pem");
const certPath = path.resolve(scriptDir, "./sslcert/cert.pem");
const receivedObj = new URL(receivedUrl);
const authCode = decodeURIComponent(
receivedObj.searchParams.get("code") || ""
);

const options = {
key: fs.readFileSync(keyPath, "utf-8"),
cert: fs.readFileSync(certPath, "utf-8"),
};
const basicAuth = Buffer.from(
`${process.env.SCHWAB_APP_KEY}:${process.env.SCHWAB_SECRET}`
).toString("base64");

const response = await fetch("https://api.schwabapi.com/v1/oauth/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${basicAuth}`,
"Accept-Encoding": "gzip",
},
body: new URLSearchParams({
grant_type: "authorization_code",
redirect_uri: callbackUrl,
code: authCode,
}),
});

// Call the function and handle the received URL
try {
const receivedUrl = await getAuthorizationResponse();
console.log(`Received URL: ${receivedUrl}`);

const receivedObj = new URL(receivedUrl);
let authcode = receivedObj.searchParams.get("code");
const code = decodeURIComponent(authcode);

const basicAuth = Buffer.from(
process.env.SCHWAB_APP_KEY + ":" + process.env.SCHWAB_SECRET,
).toString("base64");

const response = await fetch("https://api.schwabapi.com/v1/oauth/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${basicAuth}`,
"Accept-Encoding": "gzip",
},
body: new URLSearchParams({
grant_type: "authorization_code",
redirect_uri: callbackUrl,
code: code,
}),
});
if (!response.ok) {
throw new Error(
`Failed to generate token: ${response.status} - ${response.statusText}`
);
}

if (!response.ok) {
throw new Error(
`Error -- failed to generate token: ${response.status} - ${response.statusText}`,
);
const data = await response.json();
updateEnv(data.refresh_token);
} catch (error) {
console.error("Error during authorization:", error);
}

const data = await response.json();
updateEnv(data.refresh_token);
} catch (error) {
throw new Error("Error -- authorize.js failed:", JSON.stringify(error));
}
})();

0 comments on commit 0d124d6

Please sign in to comment.