UNPKG

profoundjs

Version:

Profound.js Framework and Server

263 lines (226 loc) 9.76 kB
/** * Utilities to handle user authentication with an OAuth2 provider through a sample user interface. * The code in useful-app.json or authpage.js depends on this module. */ module.exports = {}; const mod = module.exports; const workspaceCfg = require("./settings.js"); mod.provider = workspaceCfg.provider; if (typeof mod.provider !== "string" || mod.provider.length < 1) { throw new Error("provider was not specified in oauth2sample config"); } const provCfg = workspaceCfg.providerSettings[mod.provider]; if (typeof provCfg !== "object" || provCfg === null) { throw new Error("oauth2sample config provider did not have matching providerSettings entry"); } // Gather the server scheme, HTTP or HTTPS, and secure or insecure port. // WARNING: Always use encryption via HTTPS for production servers. let scheme = "https"; let port = profound.settings.securePort; if (port === 443) { port = ""; } else if (isNaN(port)) { scheme = "http"; port = (profound.settings.port === 80 ? "" : ":" + profound.settings.port); } else { port = ":" + port; } // PROTOCOL_HOST_PORT becomes a URL for the OAuth2 redirect URL and other components in the sample app. mod.PROTOCOL_HOST_PORT = `${scheme}://${workspaceCfg.serverDomainName}${port}`; // This is the route in this sample application that handles the OAuth2 server's redirection of the // client browser after authenticating or fetching an authentication code. // Each application can have its own redirect URL. mod.redirectURI = mod.PROTOCOL_HOST_PORT + "/run/oauth2sample/authpage?redir=1"; mod.startURI = mod.PROTOCOL_HOST_PORT + "/run/oauth2sample/authpage"; // Use provider-dependent settings if set, or use defaults. mod.tokenUrlContentType = typeof provCfg.tokenUrlContentType === "string" ? provCfg.tokenUrlContentType : ""; mod.authMethod = typeof provCfg.authMethod === "string" ? provCfg.authMethod : "GET"; mod.tokenNoSecret = provCfg.tokenNoSecret === true; // Headers needed by many APIs of popular OAuth2 providers. const HTTP_REQ_HEADERS = { "Accept": "application/json", "Content-Type": "application/json" }; if (typeof provCfg["User-Agent"] === "string") HTTP_REQ_HEADERS["User-Agent"] = provCfg["User-Agent"]; mod.client_id = provCfg.client_id; mod.client_secret = provCfg.client_secret; /** * Exchange authorization code for OAuth tokens. * Pre-condition: the RDF application has already validated the state. * @param {String} code * @param {undefined|String} codver code_verifier (for PKCE for some providers) * @returns {Object} with properties: access_token, expires_in, refresh_token */ mod.fetchTokensAsync = async function(code, codver) { // Assemble OAuth2 standard parameters. const requestData = { "grant_type": "authorization_code", "redirect_uri": this.redirectURI, "client_id": this.client_id, "client_secret": this.client_secret, code }; if (typeof codver === "string") { requestData.code_verifier = codver; } if (this.tokenNoSecret) { // MS AD does not allow you to send client_secret. but GitHub requires it. Implementation dependent. delete requestData.client_secret; } const headers = JSON.parse(JSON.stringify(HTTP_REQ_HEADERS)); // copy object. headers.Origin = mod.PROTOCOL_HOST_PORT; // Some providers use a different content-type header. const tokenUrlContentType = this.tokenUrlContentType; if (typeof tokenUrlContentType === "string" && tokenUrlContentType.length > 0) { headers["Content-Type"] = tokenUrlContentType; } let body; const contentType = headers["Content-Type"]; if (typeof contentType === "string" && contentType.indexOf("application/x-www-form-urlencoded") === 0) { body = ""; let sep = ""; for (const prop in requestData) { body += `${sep}${prop}=${requestData[prop]}`; sep = "&"; } } else { body = JSON.stringify(requestData); } let responseData; if (typeof this.tokenUrl !== "string" || this.tokenUrl.length < 1) { throw new Error(`This sample app requires a "tokenUrl" property to be defined for the OAuth2 provider. Please check your openapi.json file and compare with the sample in the README.md file.`); } try { responseData = await profound.httpRequest({ method: "POST", uri: this.tokenUrl, body, json: true, headers, alwaysReadBody: true }); } catch (err) { throw new Error(`Unable to fetch token from remote host: ${this.tokenUrl}`, { cause: err }); } if (typeof responseData !== "object" || responseData === null) { throw new Error("Request for OAuth2 token was missing expected object."); } if (typeof responseData.access_token !== "string") { throw new Error("Request for OAuth2 token was missing expected access_token.", { cause: responseData }); } return responseData; }; /** * Request an object with user identification by providing a user access token. * @param {String} accessToken * @returns {String} * @throws */ mod.getUserIdAsync = async function(accessToken) { const userinfoUrl = this["x-userinfoUrl"]; if (typeof userinfoUrl !== "string" || userinfoUrl.length < 1) { throw new Error("No user info URL specified"); } // Use access token to request user info from the resource server. let userInfo; try { const headers = JSON.parse(JSON.stringify(HTTP_REQ_HEADERS)); // copy object. headers.Authorization = "Bearer " + accessToken; userInfo = await profound.httpRequest({ method: "GET", uri: userinfoUrl, json: true, headers, alwaysReadBody: true }); } catch (err) { throw new Error(`Unable to get user info from remote host: ${userinfoUrl}`, { cause: err }); } if (typeof userInfo !== "object" || userInfo === null) { throw new Error("Response for user info request was invalid."); } const userField = this["x-userinfoField"]; const userid = userInfo[userField]; if (typeof userid !== "string" || userid.length < 1) { throw new Error("User info could not be read. Found: " + JSON.stringify(userInfo)); } return userid; }; // // Read OpenAPI OAuth2 properties from an openapi.json config file, and assign the properties to the module object. // // Get the User Info URL from the openapi.json file. const path = require("path"); const filePath = path.join(profound.dir, (profound.DEV ? "profoundjs" : ""), "openapi.json"); const openAPIConfig = require(filePath).components.securitySchemes; // Prevent the openapi.json module from being cached; changes should be permitted without restarting PJS. let resolvedMod = require.resolve(filePath); delete require.cache[resolvedMod]; // Find a security scheme in openapi.json for OAuth2. (This sample code assumes one has a description or key matching mod.provider.) let oaSecurityScheme; let lastOaSecurityScheme; for (const secSchemeName in openAPIConfig) { const secScheme = openAPIConfig[secSchemeName]; if (typeof secScheme === "object" && secScheme !== null && typeof secScheme.type === "string" && secScheme.type.length > 0) { if (secScheme.type === "oauth2") { lastOaSecurityScheme = secScheme; let secSchemeDescr = secScheme.description; if (typeof secSchemeDescr !== "string") secSchemeDescr = ""; else secSchemeDescr = secSchemeDescr.toLowerCase(); const lcProv = mod.provider.toLowerCase(); const secSchLC = secSchemeName.toLowerCase(); // Try to find the setting matching the provider, either in the "description" property or the key name. if (secSchLC.indexOf(lcProv) >= 0 || secSchemeDescr.indexOf(lcProv) >= 0) { // Use the first OAuth2 provider found in openapi.json that matches PROVIDER. oaSecurityScheme = secScheme; break; } } } } const hint = " Check the openapi.json configuration file."; if (typeof oaSecurityScheme !== "object" || oaSecurityScheme === null) { if (typeof lastOaSecurityScheme === "object" && lastOaSecurityScheme !== null) { // If the name of the provider could not match any description in openapi.json, then // use the last valid OAuth2 entry. oaSecurityScheme = lastOaSecurityScheme; } else { throw new Error(`Failed to find a security scheme of type oauth2.` + hint); } } const flows = oaSecurityScheme.flows; if (typeof flows !== "object" || flows === null) { throw new Error("'flows' property is missing." + hint); } const authCode = oaSecurityScheme.flows.authorizationCode; if (typeof authCode !== "object" || authCode === null) { throw new Error("'authorizationCode' property is missing." + hint); } // Scopes are required for identifying to the OAuth2 provider what resources are requested on behalf of the user. // The requested scopes are the property names of the object defined in the "scopes" property in openapi.json. // The "scope" data passed to APIs is expected to be a space-delimited list. // See: https://swagger.io/specification/ const scopes = authCode.scopes; mod.scope = typeof scopes === "object" && scopes !== null ? Object.keys(scopes).join(" ") : ""; // For convenience, copy these properties from the openapi.json into exported properties of this module. // (PAPI security store user validation needs these properties defined in openapi.json.) const copyList = ["authorizationUrl", "tokenUrl", "refreshUrl", "x-userinfoUrl", "x-userinfoField"]; copyList.forEach(el => { const exportKey = el; if (typeof authCode[el] === "string" || typeof authCode[el] === "boolean") { mod[exportKey] = authCode[el]; } else { mod[exportKey] = ""; } }); // Make sure oautils is not cached so that changes to config.js can be seen without restarting the PJS server. resolvedMod = require.resolve(__filename); delete require.cache[resolvedMod];