UNPKG

node-red-contrib-oauth2

Version:

The node-red-contrib-oauth2 is a Node-RED node that provides an OAuth2 authentication flow. This node uses the OAuth2 protocol to obtain an access token, which can be used to make authenticated API requests.

448 lines (386 loc) 17.7 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>JSDoc: Source: oauth2.js</title> <script src="scripts/prettify/prettify.js"> </script> <script src="scripts/prettify/lang-css.js"> </script> <!--[if lt IE 9]> <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script> <![endif]--> <link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css"> <link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css"> </head> <body> <div id="main"> <h1 class="page-title">Source: oauth2.js</h1> <section> <article> <pre class="prettyprint source linenums"><code>module.exports = function (RED) { 'use strict'; const axios = require('axios'); const http = require('http'); const https = require('https'); const { URLSearchParams } = require('url'); // Use URLSearchParams for form data const Logger = require('node-red-contrib-oauth2/src/libs/logger'); /** * Class representing an OAuth2 Node. */ class OAuth2Node { /** * Create an OAuth2Node. * @param {Object} config - Node configuration object. */ constructor(config) { RED.nodes.createNode(this, config); this.logger = new Logger({ name: 'identifier', count: null, active: config.debug || false, label: 'debug' }); this.logger.debug('Constructor: Initializing node with config', config); // Node configuration properties this.name = config.name || ''; this.container = config.container || ''; this.access_token_url = config.access_token_url || ''; this.redirect_uri = config.redirect_uri || ''; this.grant_type = config.grant_type || ''; this.refresh_token = config.refresh_token || ''; this.username = config.username || ''; this.password = config.password || ''; this.client_id = config.client_id || ''; this.client_secret = config.client_secret || ''; this.scope = config.scope || ''; this.resource = config.resource || ''; this.state = config.state || ''; this.rejectUnauthorized = config.rejectUnauthorized || false; this.client_credentials_in_body = config.client_credentials_in_body || false; this.headers = config.headers || {}; this.sendErrorsToCatch = config.senderr || false; // Proxy settings from environment variables or configuration this.prox = process.env.http_proxy || process.env.HTTP_PROXY || config.proxy; this.noprox = (process.env.no_proxy || process.env.NO_PROXY || '').split(','); this.logger.debug('Constructor: Finished setting up node properties'); // Register the input handler this.on('input', this.onInput.bind(this)); this.host = RED.settings.uiHost || 'localhost'; this.logger.debug('Constructor: Node input handler registered'); } /** * Handles input messages. * @param {Object} msg - Input message object. * @param {Function} send - Function to send messages. * @param {Function} done - Function to indicate processing is complete. */ async onInput(msg, send, done) { // this.debug ? this.logger.setOn() : this.logger.setOff(); this.logger.debug('onInput: Received message', msg); const options = this.generateOptions(msg); // Generate request options this.logger.debug('onInput: Generated request options', options); this.configureProxy(); // Configure proxy settings this.logger.debug('onInput: Configured proxy settings', this.prox); delete msg.oauth2Request; // Remove oauth2Request from msg this.logger.debug('onInput: Removed oauth2Request from message'); options.form = this.cleanForm(options.form); // Clean the form data this.logger.debug('onInput: Cleaned form data', options.form); try { const response = await this.makePostRequest(options); // Make the POST request this.logger.debug('onInput: POST request response', response); this.handleResponse(response, msg, send); // Handle the response } catch (error) { this.logger.error('onInput: Error making POST request', error); this.handleError(error, msg, send); // Handle any errors } done(); // Indicate that processing is complete this.logger.debug('onInput: Finished processing input'); } /** * Generates options for the HTTP request. * @param {Object} msg - Input message object. * @returns {Object} - The request options. */ generateOptions(msg) { // Log the start of the option generation process with the input message this.logger.debug('generateOptions: Configuring options with message', msg); // Initialize the form object to hold the form data let form = {}; // Set the default URL to the access token URL configured in the node let url = this.access_token_url; // Initialize headers with default Content-Type and Accept headers let headers = { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' }; // Retrieve credentials from the message if available, otherwise use an empty object const creds = msg.oauth2Request ? msg.oauth2Request.credentials || {} : {}; // Initialize the form data with grant_type, scope, resource, and state form = { grant_type: creds.grant_type || this.grant_type, scope: creds.scope || this.scope, resource: creds.resource || this.resource, state: creds.state || this.state }; // Define functions for different OAuth2 flows const flows = { // Password flow function password: () => { this.logger.debug('generateOptions: Password flow detected'); form.username = creds.username || this.username; form.password = creds.password || this.password; }, // Client credentials flow function client_credential: () => { this.logger.debug('generateOptions: Client credentials flow detected'); form.client_id = creds.client_id || this.client_id; form.client_secret = creds.client_secret || this.client_secret; }, // Refresh token flow function refresh_token: () => { this.logger.debug('generateOptions: Refresh token flow detected'); form.client_id = creds.client_id || this.client_id; form.client_secret = creds.client_secret || this.client_secret; form.refresh_token = creds.refresh_token || this.refresh_token; }, // Authorization code flow function authorization_code: () => { this.logger.debug('generateOptions: Authorization code flow detected'); const credentials = RED.nodes.getCredentials(this.id) || {}; if (credentials) { form.code = credentials.code; form.redirect_uri = this.redirect_uri; } }, // Implicit flow function implicit_flow: () => { this.logger.debug('generateOptions: Implicit flow detected'); const credentials = RED.nodes.getCredentials(this.id) || {}; if (credentials) { form.client_id = this.client_id; form.client_secret = this.client_secret; form.code = credentials.code; form.grant_type = 'authorization_code'; form.redirect_uri = this.redirect_uri; } }, // Set by credentials function set_by_credentials: () => { this.logger.debug('generateOptions: Set by credentials flow detected'); if (msg.oauth2Request) { const credentials = msg.oauth2Request.credentials || {}; form.client_id = credentials.client_id || this.client_id; form.client_secret = credentials.client_secret || this.client_secret; form.refresh_token = credentials.refresh_token || ''; } } }; // Check if the grant type from the credentials is supported and call the corresponding function if (creds.grant_type &amp;&amp; flows[creds.grant_type]) { flows[creds.grant_type](); } // Check if the default grant type of the node is supported and call the corresponding function else if (this.grant_type &amp;&amp; flows[this.grant_type]) { flows[this.grant_type](); } // Check if client credentials should be included in the body if (this.client_credentials_in_body) { this.logger.debug('generateOptions: Client credentials in body detected, using credentials'); form.client_id = creds.client_id || this.client_id; form.client_secret = creds.client_secret || this.client_secret; } else { // Otherwise, add the Authorization header with client credentials encoded in base64 headers.Authorization = 'Basic ' + Buffer.from(`${creds.client_id || this.client_id}:${creds.client_secret || this.client_secret}`).toString('base64'); } // Set the URL to the access token URL from the message if available, otherwise use the default url = msg.oauth2Request ? msg.oauth2Request.access_token_url || this.access_token_url : this.access_token_url; // Log the final generated options this.logger.debug('generateOptions: Returning options', { method: 'POST', url, headers, form }); // Return the HTTP request options return { method: 'POST', url: url, headers: { ...headers, ...this.headers }, rejectUnauthorized: this.rejectUnauthorized, form: form }; } /** * Configures proxy settings. */ configureProxy() { if (!this.prox) return; const proxyURL = new URL(this.prox); this.proxy = { protocol: proxyURL.protocol, hostname: proxyURL.hostname, port: proxyURL.port, username: proxyURL.username || null, password: proxyURL.password || null }; this.logger.debug('configureProxy: Proxy configured', this.proxy); } /** * Cleans form data by removing undefined or empty values. * @param {Object} form - The form data. * @returns {Object} - The cleaned form data. */ cleanForm(form) { const cleanedForm = Object.fromEntries(Object.entries(form).filter(([, value]) => value !== undefined &amp;&amp; value !== '')); this.logger.debug('cleanForm: Cleaned form data', cleanedForm); return cleanedForm; } /** * Makes a POST request. * @param {Object} options - The request options. * @returns {Promise&lt;Object>} - The response from the request. */ async makePostRequest(options) { this.logger.debug('makePostRequest: Making POST request with options', options); const axiosOptions = { method: options.method, url: options.url, headers: options.headers, data: new URLSearchParams(options.form).toString(), proxy: false, httpAgent: new http.Agent({ rejectUnauthorized: options.rejectUnauthorized }), httpsAgent: new https.Agent({ rejectUnauthorized: options.rejectUnauthorized }) }; if (this.proxy) { const HttpsProxyAgent = require('https-proxy-agent'); axiosOptions.httpsAgent = new HttpsProxyAgent(this.proxy); } this.logger.debug('makePostRequest: Axios request options prepared', axiosOptions); return axios(axiosOptions).catch((error) => { this.logger.error('makePostRequest: Error during POST request', error); throw error; }); } /** * Handles the response from the POST request. * @param {Object} response - The response object. * @param {Object} msg - Input message object. * @param {Function} send - Function to send messages. */ handleResponse(response, msg, send) { this.logger.debug('handleResponse: Handling response', response); if (!response || !response.data) { this.logger.warn('handleResponse: Invalid response data', response); this.handleError({ message: 'Invalid response data' }, msg, send); return; } msg.oauth2Response = response.data || {}; msg.headers = response.headers || {}; // Include headers in the message this.setStatus('green', `HTTP ${response.status}, ok`); this.logger.debug('handleResponse: Response data set in message', msg); send(msg); } /** * Handles errors from the POST request. * @param {Object} error - The error object. * @param {Object} msg - Input message object. * @param {Function} send - Function to send messages. */ handleError(error, msg, send) { this.logger.error('handleError: Handling error', error); const status = error.response ? error.response.status : error.code; const message = error.response ? error.response.statusText : error.message; const data = error.response &amp;&amp; error.response.data ? error.response.data : {}; const headers = error.response ? error.response.headers : {}; msg.oauth2Error = { status, message, data, headers }; this.setStatus('red', `HTTP ${status}, ${message}`); this.logger.debug('handleError: Error data set in message', msg); if (this.sendErrorsToCatch) { send([null, msg]); } else { this.error(msg); send([null, msg]); } } /** * Sets the status of the node. * @param {string} color - The color of the status indicator. * @param {string} text - The status text. */ setStatus(color, text) { this.logger.debug('setStatus: Setting status', { color, text }); this.status({ fill: color, shape: 'dot', text }); setTimeout(() => { this.status({}); }, 250); } } /** * Endpoint to retrieve OAuth2 credentials based on a token. * @param {Object} req - The request object. * @param {Object} res - The response object. */ RED.httpAdmin.get('/oauth2/credentials/:token', (req, res) => { try { const credentials = RED.nodes.getCredentials(req.params.token); if (credentials) { res.json({ code: credentials.code, redirect_uri: credentials.redirect_uri }); } else { res.status(404).send('oauth2.error.no-credentials'); } } catch (error) { res.status(500).send('oauth2.error.server-error'); } }); /** * Endpoint to handle OAuth2 redirect and store the authorization code. * @param {Object} req - The request object. * @param {Object} res - The response object. */ RED.httpAdmin.get('/oauth2/redirect', (req, res) => { if (req.query.code) { const [node_id] = req.query.state.split(':'); let credentials = RED.nodes.getCredentials(node_id); if (!credentials) { credentials = {}; } credentials = { ...credentials, ...req.query }; RED.nodes.addCredentials(node_id, credentials); res.send(` &lt;HTML> &lt;HEAD> &lt;script language="javascript" type="text/javascript"> function closeWindow() { window.open('', '_parent', ''); window.close(); } function delay() { setTimeout("closeWindow()", 1000); } &lt;/script> &lt;/HEAD> &lt;BODY onload="javascript:delay();"> &lt;p>Success! This page can be closed if it doesn't do so automatically.&lt;/p> &lt;/BODY> &lt;/HTML> `); } else { res.status(400).send('oauth2.error.no-credentials'); } }); // Register the OAuth2Node node type RED.nodes.registerType('oauth2', OAuth2Node, { credentials: { clientId: { type: 'text' }, clientSecret: { type: 'password' }, accessToken: { type: 'password' }, refreshToken: { type: 'password' }, expireTime: { type: 'password' }, code: { type: 'password' } } }); }; </code></pre> </article> </section> </div> <nav> <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="OAuth2Node.html">OAuth2Node</a></li></ul> </nav> <br class="clear"> <footer> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.3</a> on Fri May 24 2024 08:34:19 GMT-0300 (Brasilia Standard Time) </footer> <script> prettyPrint(); </script> <script src="scripts/linenumber.js"> </script> </body> </html>