Skip to content

Commit

Permalink
Add static loopback pages (#202)
Browse files Browse the repository at this point in the history
Co-authored-by: Ben Polinsky <[email protected]>
  • Loading branch information
ben-polinsky and ben-polinsky authored Aug 22, 2023
1 parent fa4e06b commit 37f1da3
Show file tree
Hide file tree
Showing 9 changed files with 514 additions and 94 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "add styled html loopback pages",
"packageName": "@itwin/electron-authorization",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "add styled html loopback pages",
"packageName": "@itwin/node-cli-authorization",
"email": "[email protected]",
"dependentChangeType": "patch"
}
8 changes: 5 additions & 3 deletions packages/electron/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@
}
},
"scripts": {
"build": "npm run -s build:cjs && npm run -s build:esm",
"build": "pnpm run -s build:cjs && pnpm run -s build:esm && pnpm run copy:assets",
"build:cjs": "tsc 1>&2 -p tsconfig.cjs.json",
"build:esm": "tsc 1>&2 -p tsconfig.esm.json",
"copy:assets": "cpx src/static/* dist/main/static && cpx src/static/* lib/cjs/main/static && cpx src/static/* lib/esm/main/static",
"cover": "nyc npm test",
"clean": "rimraf lib",
"docs": "RUSHSTACK_FILE_ERROR_BASE_FOLDER='../..' betools docs --includes=../../generated-docs/extract --json=../../generated-docs/auth-clients/electron-authorization/file.json --tsIndexFile=./docsIndex.ts --onlyJson",
Expand Down Expand Up @@ -50,13 +51,14 @@
"@itwin/core-bentley": "^3.7.0",
"@itwin/eslint-plugin": "^3.3.0",
"@playwright/test": "~1.35.1",
"@types/chai-as-promised": "^7.1.1",
"@types/chai": "^4.2.22",
"@types/chai-as-promised": "^7.1.1",
"@types/mocha": "^8.2.3",
"@types/node": "^16.0.0",
"@types/sinon": "^10.0.13",
"chai-as-promised": "^7.1.1",
"chai": "^4.2.22",
"chai-as-promised": "^7.1.1",
"cpx2": "^5.0.0",
"dotenv": "~16.0.3",
"electron": "^26.0.0",
"eslint": "^7.32.0",
Expand Down
91 changes: 70 additions & 21 deletions packages/electron/src/main/LoopbackWebServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
// Code based on the blog article @ https://authguidance.com

import * as Http from "http";
import * as path from "path";
import { readFileSync } from "fs";
import type { AuthorizationErrorJson, AuthorizationResponseJson } from "@openid/appauth";
import type { ElectronAuthorizationEvents } from "./Events";
import { Logger } from "@itwin/core-bentley";
import { assert, Logger } from "@itwin/core-bentley";
const loggerCategory = "electron-auth";

type StateEventsPair = [string, ElectronAuthorizationEvents];
Expand All @@ -34,6 +36,20 @@ class AuthorizationState {
}
}

interface HtmlTemplateParams {
pageTitle: string;
contentTitle: string;
contentMessage: string;
}

interface OidcUrlSearchParams {
state: string | null;
code: string | null;
error: string | null;
errorUri: string | null;
errorDescription: string | null;
}

/**
* Web server to listen to authorization requests/responses for the DesktopAuthorizationClient
* @internal
Expand Down Expand Up @@ -76,25 +92,18 @@ export class LoopbackWebServer {
else
LoopbackWebServer._httpServer = undefined;
});

}

/** Listen/Handle browser events */
private static onBrowserRequest(httpRequest: Http.IncomingMessage, httpResponse: Http.ServerResponse): void {
if (!httpRequest.url)
return;

// Parse the request URL to determine the authorization code, state and errors if any
const redirectedUrl = new URL(httpRequest.url, LoopbackWebServer._redirectUri);
const searchParams = redirectedUrl.searchParams;
const { state, code, error, errorUri, errorDescription } = LoopbackWebServer.parseUrlSearchParams(httpRequest.url);

const state = searchParams.get("state") || undefined;
const code = searchParams.get("code");
const error = searchParams.get("error");
if (!state) {
// ignore irrelevant requests (e.g. favicon.ico)
// ignore irrelevant requests (e.g. favicon.ico)
if (!state)
return;
}

// Look up context for the corresponding outgoing request
const authorizationEvents = LoopbackWebServer._authState.getEvents(state);
Expand All @@ -104,24 +113,64 @@ export class LoopbackWebServer {
// Notify listeners of the code response or error
let authorizationResponse: AuthorizationResponseJson | null = null;
let authorizationError: AuthorizationErrorJson | null = null;
let httpResponseContent: HtmlTemplateParams;

httpResponse.writeHead(200, { "Content-Type": "text/html" }); // eslint-disable-line @typescript-eslint/naming-convention

if (error) {
const errorUri = searchParams.get("error_uri") || undefined;
const errorDescription = searchParams.get("error_description") || undefined;
authorizationError = { error, error_description: errorDescription, error_uri: errorUri, state }; // eslint-disable-line @typescript-eslint/naming-convention
httpResponse.write("<h1>Sign in error!</h1>"); // TODO: Needs localization
httpResponse.end();
authorizationError = { error, error_description: errorDescription ?? undefined, error_uri: errorUri ?? undefined, state }; // eslint-disable-line @typescript-eslint/naming-convention
httpResponseContent = {
pageTitle: "iTwin Auth Sign in error",
contentTitle: "Sign in Error",
contentMessage: "Please check your application's error console.",
};
// TODO: Needs localization
} else {
authorizationResponse = { code: code!, state };
httpResponse.writeHead(200, { "Content-Type": "text/html" }); // eslint-disable-line @typescript-eslint/naming-convention
httpResponse.write("<h1>Sign in was successful!</h1>You can close this browser window and return to the application"); // TODO: Needs localization
httpResponse.end();
assert(!!code, "Auth response code is not present");
authorizationResponse = { code, state };
httpResponseContent = {
pageTitle: "iTwin Auth - Sign in successful",
contentTitle: "Sign in was successful!",
contentMessage: "You can close this browser window and return to the application.",
};
}

const html = LoopbackWebServer.getHtmlTemplate(
httpResponseContent
);

httpResponse.write(html);
httpResponse.end();
authorizationEvents.onAuthorizationResponse.raiseEvent(authorizationError, authorizationResponse);

// Handle the authorization completed event
authorizationEvents.onAuthorizationResponseCompleted.addOnce((_authCompletedError?: AuthorizationErrorJson) => {
// Stop the web server now that the signin attempt has finished
LoopbackWebServer.stop();
});
}

private static parseUrlSearchParams(url: string): OidcUrlSearchParams {
// Parse the request URL to determine the authorization code, state and errors if any
const redirectedUrl = new URL(url, LoopbackWebServer._redirectUri);
const searchParams = redirectedUrl.searchParams;

const state = searchParams.get("state");
const code = searchParams.get("code");
const error = searchParams.get("error");
const errorUri = searchParams.get("error_uri");
const errorDescription = searchParams.get("error_description");

return {
state, code, error, errorUri, errorDescription,
};
}

private static getHtmlTemplate({ pageTitle, contentTitle, contentMessage }: HtmlTemplateParams): string {
let template = readFileSync(path.resolve(__filename, "..", "static", "auth-template.html"), "utf-8");
template = template.replace("{--pageTitle--}", pageTitle);
template = template.replace("{--contentTitle--}", contentTitle);
template = template.replace("{--contentMessage--}", contentMessage);

return template;
}
}
110 changes: 110 additions & 0 deletions packages/electron/src/static/auth-template.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<html lang="en" dir="ltr">

<head>
<title>{--pageTitle--}</title>
<link rel="icon" type="image/x-icon" href="https://connect-cdn.bentley.com/cdn/en/favicon.ico" />

<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.0" />
<style type="text/css">
* {
font-family: "Open Sans", -apple-system, "system-ui", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}

html,
body {
padding: 0;
margin: 0;
}

body {
height: 100%;
}

div.page-container {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}

header {
margin: 100px 0 20px;
}

header img {
height: 52px;
}

main {
width: 480px;
border-radius: 3px;
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.2);
padding: 32px;
background-color: #FFF;
text-align: center;
}

main h1 {
margin-top: 0px;
font-size: 16px;
}

footer {
height: 90px;
margin-top: auto;
color: rgba(0, 0, 0, 0.4);
font-size: 11px;
}

footer a {
text-decoration: none;
}

footer a:hover,
footer a:active {
text-decoration: underline;
}
</style>
</head>

<body>
<div class="page-container">
<header>
<img src="https://connect-cdn.bentley.com/cdn/en/logo.svg" alt="Bentley Systems logo" />
</header>

<main>
<h1>{--contentTitle--}</h1>
<span>{--contentMessage--}</span>
</main>

<footer>
<div class="copyright">©
<script>
const year = new Date();
document.write(year.getFullYear());</script> Bentley Systems, Incorporated |
<a href="https://www.bentley.com/legal/terms-of-use-and-select-online-agreement/" target="_blank">
<span style="color: rgb(153, 153, 153)">Terms of Service</span></a>
|
<a href="https://www.bentley.com/legal/privacy-policy/" target="_blank">
<span style="color: rgb(153, 153, 153)">Privacy</span></a>
|
<a href="https://www.bentley.com/legal/terms-of-use-and-select-online-agreement/" target="_blank">
<span style="color: rgb(153, 153, 153)">Terms of Use</span></a>
|
<a href="https://www.bentley.com/legal/cookie-policy/" target="_blank">
<span style="color: rgb(153, 153, 153)">Cookies</span></a>
|
<a href="https://www.bentley.com/legal/overview/" target="_blank">
<span style="color: rgb(153, 153, 153)">Legal Notices</span></a>
<br />
</div>
</footer>
</div>
</body>

</html>
6 changes: 4 additions & 2 deletions packages/node-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"description": "Node.js command-line authorization client for iTwin platform",
"scripts": {
"build": "npm run -s build:cjs",
"build:cjs": "tsc 1>&2 --outDir lib/cjs",
"build:cjs": "tsc 1>&2 --outDir lib/cjs && npm run copy:assets",
"copy:assets": "cpx src/static/* lib/cjs/static",
"clean": "rimraf lib",
"docs": "RUSHSTACK_FILE_ERROR_BASE_FOLDER='../..' betools docs --includes=../../generated-docs/extract --json=../../generated-docs/auth-clients/node-cli-authorization/file.json --tsIndexFile=./index.ts --onlyJson",
"lint": "eslint -f visualstudio \"./src/**/*.ts\" 1>&2",
Expand Down Expand Up @@ -43,11 +44,12 @@
"@types/node": "^18.11.5",
"chai": "^4.2.22",
"chai-as-promised": "^7.1.1",
"source-map-support": "^0.5.9",
"cpx2": "^5.0.0",
"eslint": "^7.32.0",
"mocha": "^8.2.3",
"nyc": "^15.1.0",
"rimraf": "^3.0.2",
"source-map-support": "^0.5.9",
"typescript": "~5.0.2"
},
"peerDependencies": {
Expand Down
Loading

0 comments on commit 37f1da3

Please sign in to comment.