profoundjs
Version:
Profound.js Framework and Server
191 lines (168 loc) • 6.17 kB
JavaScript
/**
* Handle the initial authentication page and the redirect from the OAuth2 provider
* to the Rich Display File application, "useful-app".
*/
const crypto = require("crypto");
const HTML_HEAD = `<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, target-densitydpi=device-dpi">
<link href="/profoundui/proddata/css/profoundui.css" rel="stylesheet" type="text/css">`;
const HTML_TAIL = "</body></html>";
/**
* Route a request to this URL to either show the initial auth page, or draw the redirect page.
* @param {HTTPRequest} request
* @param {HTTPResponse} response
*/
function authpage(request, response) {
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setHeader("Expires", "0");
if (request.query.redir === "1") {
redirect(request, response);
}
else {
start(request, response);
}
}
/**
* Draw the initial auth page.
*/
function start(request, response) {
// "state" is a cryptographically strong random sequence of characters, used later to
// protect against cross-site forgery attacks.
const formdata = {
response_type: "code",
redirect_uri: "",
client_id: "",
scope: "",
state: crypto.randomUUID(),
method: "GET",
url: "",
provider: ""
};
let message = request.query.error;
// Get 64 random characters needed for PKCE code verification. The characters must be URL-safe
// so that they can be passed without alteration to verify the code later.
let code_verifier_str = "";
while (code_verifier_str.length < 64) {
const rand = crypto.randomInt(45, 127);
// permitted: - . _ ~ 0-9 A-Z a-z
if (rand === 0x2d || rand === 0x2e || rand === 0x5f || rand === 0x7e ||
(rand >= 0x30 && rand <= 0x39) ||
(rand >= 0x41 && rand <= 0x5a) || (rand >= 0x61 && rand <= 0x7a)) {
code_verifier_str += String.fromCharCode(rand);
}
}
const hash = crypto.createHash("sha256");
hash.update(code_verifier_str, "utf8"); // read the random bytes into the hash object.
let oautils;
if (typeof message !== "string") message = "";
try {
oautils = require("./oautils.js");
formdata.redirect_uri = oautils.redirectURI;
formdata.client_id = oautils.client_id;
formdata.scope = oautils.scope;
formdata.code_challenge = hash.digest("base64url"); // the hash is expected to be base64url encoding.
formdata.code_challenge_method = "S256";
if (typeof oautils.authMethod === "string" && oautils.authMethod.length > 0 && oautils.authMethod !== "GET") {
formdata.method = oautils.authMethod;
}
formdata.provider = oautils.provider;
formdata.url = oautils.authorizationUrl;
}
catch (err) {
message = String(err);
}
let urlErrStr = "";
if (typeof formdata.url !== "string" || formdata.url.length < 1) {
urlErrStr = `<p class="error">This sample app requires an "authorizationUrl" property to be defined for the OAuth2
provider. Please check your openapi.json file and compare with the sample in the README.md file.</p>`;
}
const responseHTML = `${HTML_HEAD}
<script type="text/javascript">
function init(){
/* Store value that are only available to pages in the same origin as this page. */
/* These values will persist even when the page redirects to the OAuth2 provider's site and back. */
localStorage.setItem("pjs_oadata", "${formdata.state}");
localStorage.setItem("pjs_codver", "${code_verifier_str}");
}
</script>
<style type="text/css">
p.error {
color: red;
white-space: normal;
}
pre {
color:
}
div
width: calc(100% - 20px);
left: 10px;
top: 10px;
}
width: 100px;
height: 25px;
}
</style>
</head>
<body onload="init();">
<div id="pui">
<p>Click Sign On to use your ${formdata.provider} account to access this app.</p>
<form action="${formdata.url}" method="${formdata.method}">
<input type="hidden" name="response_type" value="${formdata.response_type}" />
<input type="hidden" name="redirect_uri" value="${formdata.redirect_uri}" />
<input type="hidden" name="client_id" value="${formdata.client_id}" />
<input type="hidden" name="scope" value="${formdata.scope}" />
<input type="hidden" name="state" value="${formdata.state}" />
<input type="hidden" name="code_challenge" value="${formdata.code_challenge}" />
<input type="hidden" name="code_challenge_method" value="${formdata.code_challenge_method}" />
<input type="submit" value="Sign On" class="pui-solid-button-yes blueprint-defaults" id="submit" />
</form>
${urlErrStr}
<pre>${message}</pre>
</div>
${HTML_TAIL}`;
response.send(responseHTML);
}
/**
* Respond to a redirect from the OAuth2 authentication server.
*/
async function redirect(request, response) {
let strResp;
try {
const rquery = request.query;
if (rquery.error) {
// Pass on any errors to the user.
throw new Error(`${rquery.error}\nDescription: ${rquery.error_description}\n${rquery.error_uri}\nState: ${rquery.state}`);
}
if (typeof rquery.code !== "string" || rquery.code.length < 1 ||
typeof rquery.state !== "string" || rquery.state.length < 1) {
throw new Error("Authentication server did not provide code or state.");
}
strResp = `${HTML_HEAD}
<script type="text/javascript">
function sendTokens(){
const state = localStorage.getItem("pjs_oadata");
if (typeof state === "string" && state.length > 0 && state === "${rquery.state}"){
localStorage.setItem("pjs_oadata", "${rquery.code}");
window.location = "/run/oauth2sample/useful-app";
}
else {
document.body.innerHTML = '<b>Error:</b> Unable to authenticate. Invalid state.';
}
}
</script>
</head>
<body onload="sendTokens();">Redirecting to session...</body>
</html>`;
}
catch (err) {
strResp = `${HTML_HEAD}</head><body><pre>${err.message}\n${err.stack}</pre>${HTML_TAIL}`;
}
response.send(strResp);
}
exports.run = authpage;