diff --git a/how-to/integrate-server-authentication/README.md b/how-to/integrate-server-authentication/README.md index 0823f9d544..37a0cf90ff 100644 --- a/how-to/integrate-server-authentication/README.md +++ b/how-to/integrate-server-authentication/README.md @@ -6,7 +6,91 @@ Your OpenFin application will often need to authenticate using a server login page before use, this examples demonstrates such an integration. -This application you are about to install is a simple example of plugging in your own content or app. The basic server intercepts a request to the provider window and checks to see if you are authenticated. If you are not it redirects to a login screen. If you are then it will load the provider window. The settings in the manifest make the provider window visible (so it acts as a splash screen or the location where the login form will be displayed). When you have an authenticated session and the splash screen is visible then the provider will hide itself after a period of time and launch the main application window. This example assumes you have already [set up your development environment](https://developers.openfin.co/of-docs/docs/set-up-your-dev-environment) +This application you are about to install is a simple example of plugging in your own content or app. The basic server intercepts a request to the provider window and checks to see if you are authenticated. If you are not it redirects to a login screen. If you are then it will load the provider window. + +This example assumes you have already [set up your development environment](https://developers.openfin.co/of-docs/docs/set-up-your-dev-environment) + +There are two use cases covered: + +## Server side authentication with the platform provider window visible + +The settings in the [manifest.fin.json](./public/manifest.fin.json) make the provider window visible (so it acts as a splash screen or the location where the login form will be displayed). When you have an authenticated session and the splash screen is visible then the provider will hide itself after a period of time and launch the main application window. + +```json +"platform": { + ... + "autoShow": true, + "providerUrl": "http://localhost:8080/platform/provider.html", + }, +``` + +## Server side authentication with the platform provider window invisible + +The settings in the [second.manifest.fin.json](./public/second.manifest.fin.json) make the provider window invisible. When the server redirects to a login screen then a preload script which is loaded into the provider window checks to see if the window should be visible (if it is the login screen) or if an error window should be shown (if it isn't the login screen and it isn't the provider window....i.e. it has become stuck as part of a redirect). + +```json +"platform": { + ... + "autoShow": false, + "providerUrl": "http://localhost:8080/platform/provider.html", + "preloadScripts": [ + { + "url": "http://localhost:8080/preload/auth-preload-check.js" + } + ] + }, +``` + +The preload script needs to be in an area that does not require authentication (as you haven't been authenticated yet if you are on the login screen). + +The [preload script](./public/preload/auth-preload-check.js) is an example and should not be treated as production code: + +```javascript +document.addEventListener('DOMContentLoaded', async () => { + console.log('auth-preload-check.js loaded. Performing logic checks.'); + // preload scripts can be loaded into an iframe so only check the top level window + if (window === window.top && window.fin !== undefined) { + // TODO: ADD YOUR OWN LOGIC HERE + console.log('auth-preload-check.js logic starting.'); + // Create a new URL object from the current window location + const url = new URL(window.location.href); + + // TODO: ADD YOUR OWN PATH LOGIC HERE + // determine behavior based on the current URL (we have example paths) + if (url.pathname === '/app/login') { + console.log('Detected we are on the login page.'); + // If we are on the login page ensure the page is visible + await fin.me.show(); + } else { + // ensure the page is hidden as we may have shown it if it was the login page and we are now on a redirect page or the provider. + console.log('We are on a page that should not be visible. Ensuring the window is hidden.'); + await fin.me.hide(); + } + + // TODO: WHEN STUCK OR UNHAPPY PATH DETERMINE WHAT TO DO NEXT + // We provide an example of launching a new window to show a friendly error message + if (url.pathname === '/app/stuck') { + console.log( + 'Detected we are authenticated but a redirect has encountered an error and is stuck so the main provider.html page will not be loaded. Showing a friendly error message.' + ); + window.open('/app/friendly-error', '_blank'); + } + } +}); +``` + +## Login Accounts + +There are two login accounts that simulate a successful scenario and a stuck scenario. The stuck scenario is a redirect page the platform doesn't control so when it happens the manifest.fin.json file will be stuck on that page. The second.manifest.fin.json file uses a preload script that detects it is on a stuck page and launches a user error page. + +- Success account: `test@example.com / pass1234` +- Stuck account: `stuck@example.com / pass1234` + +## Things to note + +You may have different paths depending on environment. You might decide to have environment based preload scripts that are assigned to the environment specific manifest file. + +Consider the unhappy path. What would be helpful to your users and your support team if a user could not login to your platform. In our example we show a pop up window but your business/product owner should be involved in the conversation. ## Running the Sample @@ -44,14 +128,26 @@ npm run start npm run client ``` +or to run the hidden provider window using a preload script: + +```shell +npm run secondclient +``` + 5. If you modify and want to build the code you can run the build command. ```shell npm run build ``` +## Scenario 1 - manifest.fin.json - Visible Provider Window + ![Server Authentication](openfin-integrate-server-authentication.gif) +## Scenario 2 - second.manifest.fin.json - Invisible Provider Window + +![Server Authentication Hidden Provider](openfin-integrate-server-authentication-hidden.gif) + --- ### Read more about [working with Workspace](https://developers.openfin.co/of-docs/docs/overview-of-workspace) diff --git a/how-to/integrate-server-authentication/openfin-integrate-server-authentication-hidden.gif b/how-to/integrate-server-authentication/openfin-integrate-server-authentication-hidden.gif new file mode 100644 index 0000000000..6afa90f8b7 Binary files /dev/null and b/how-to/integrate-server-authentication/openfin-integrate-server-authentication-hidden.gif differ diff --git a/how-to/integrate-server-authentication/openfin-integrate-server-authentication.gif b/how-to/integrate-server-authentication/openfin-integrate-server-authentication.gif index d4003c78e5..520795274a 100644 Binary files a/how-to/integrate-server-authentication/openfin-integrate-server-authentication.gif and b/how-to/integrate-server-authentication/openfin-integrate-server-authentication.gif differ diff --git a/how-to/integrate-server-authentication/package.json b/how-to/integrate-server-authentication/package.json index 65ea4bd81f..b345139d25 100644 --- a/how-to/integrate-server-authentication/package.json +++ b/how-to/integrate-server-authentication/package.json @@ -7,6 +7,7 @@ "dos": "node ./scripts/dos.mjs && node ./scripts/kill.mjs", "kill": "node ./scripts/kill.mjs", "client": "node ./scripts/launch.mjs", + "secondclient": "node ./scripts/launch.mjs http://localhost:8080/second.manifest.fin.json", "build-client": "webpack build --config ./client/webpack.config.js --mode=development", "build-server": "tsc --project ./server", "build": "npm run build-server && npm run build-client", diff --git a/how-to/integrate-server-authentication/public/app/friendly-error.html b/how-to/integrate-server-authentication/public/app/friendly-error.html new file mode 100644 index 0000000000..8920e817fb --- /dev/null +++ b/how-to/integrate-server-authentication/public/app/friendly-error.html @@ -0,0 +1,52 @@ + + + + + + Friendly Error Page + + + + + + + +
+
+

Friendly Error Page

+

We are on the unhappy path.

+
+
+ OpenFin +
+
+
+

+ The provider has not been loaded due to an error so the platform cannot perform the necessary + bootstrapping steps to launch the platform. +

+

If you logout and exit you will be presented with the login screen again.

+

If you just exit you will be presented with this screen again.

+
+ + + + + diff --git a/how-to/integrate-server-authentication/public/app/login.html b/how-to/integrate-server-authentication/public/app/login.html index 2933ed1d56..ff89de1642 100644 --- a/how-to/integrate-server-authentication/public/app/login.html +++ b/how-to/integrate-server-authentication/public/app/login.html @@ -23,8 +23,13 @@

Demonstrate integrating Server Authentication.

- This example only has one set of credentials that work - test@example.com / pass1234 + This example only has one set of credentials that work: + test@example.com / pass1234 +

+

+ The other set of credentials will authenticate you but then simulate a failure so that you are stuck + in a redirect before the provider page is loaded (you can logout and exit to clear this session): + stuck@example.com / pass1234

Enter your credentials to login:

diff --git a/how-to/integrate-server-authentication/public/app/stuck.html b/how-to/integrate-server-authentication/public/app/stuck.html new file mode 100644 index 0000000000..270ea18c4b --- /dev/null +++ b/how-to/integrate-server-authentication/public/app/stuck.html @@ -0,0 +1,51 @@ + + + + + + Stuck Redirect + + + + + + + +
+
+

Stuck Redirect

+

An example of a window that the platform doesn't control.

+
+
+ OpenFin +
+
+
+

+ An example assuming you have logged in and are authenticated or you were already authenticated but + something went wrong before you were redirected to the main provider page. +

+

If you logout and exit you will be presented with the login screen again.

+

If you just exit you will be presented with this screen again.

+
+
+ + +
+ + + diff --git a/how-to/integrate-server-authentication/public/preload/auth-preload-check.js b/how-to/integrate-server-authentication/public/preload/auth-preload-check.js new file mode 100644 index 0000000000..8b50e447e7 --- /dev/null +++ b/how-to/integrate-server-authentication/public/preload/auth-preload-check.js @@ -0,0 +1,31 @@ +document.addEventListener('DOMContentLoaded', async () => { + console.log('auth-preload-check.js loaded. Performing logic checks.'); + // preload scripts can be loaded into an iframe so only check the top level window + if (window === window.top && window.fin !== undefined) { + // TODO: ADD YOUR OWN LOGIC HERE + console.log('auth-preload-check.js logic starting.'); + // Create a new URL object from the current window location + const url = new URL(window.location.href); + + // TODO: ADD YOUR OWN PATH LOGIC HERE + // determine behavior based on the current URL (we have example paths) + if (url.pathname === '/app/login') { + console.log('Detected we are on the login page.'); + // If we are on the login page ensure the page is visible + await fin.me.show(); + } else { + // ensure the page is hidden as we may have shown it if it was the login page and we are now on a redirect page or the provider. + console.log('We are on a page that should not be visible. Ensuring the window is hidden.'); + await fin.me.hide(); + } + + // TODO: WHEN STUCK OR UNHAPPY PATH DETERMINE WHAT TO DO NEXT + // We provide an example of launching a new window to show a friendly error message + if (url.pathname === '/app/stuck') { + console.log( + 'Detected we are authenticated but a redirect has encountered an error and is stuck so the main provider.html page will not be loaded. Showing a friendly error message.' + ); + window.open('/app/friendly-error', '_blank'); + } + } +}); diff --git a/how-to/integrate-server-authentication/public/second.manifest.fin.json b/how-to/integrate-server-authentication/public/second.manifest.fin.json new file mode 100644 index 0000000000..0032abd9f2 --- /dev/null +++ b/how-to/integrate-server-authentication/public/second.manifest.fin.json @@ -0,0 +1,45 @@ +{ + "devtools_port": 9090, + "licenseKey": "openfin-demo-license-key", + "runtime": { + "arguments": "", + "version": "38.126.83.79" + }, + "platform": { + "uuid": "integrate-server-authentication-hidden", + "icon": "http://localhost:8080/favicon.ico", + "autoShow": false, + "providerUrl": "http://localhost:8080/platform/provider.html", + "preventQuitOnLastWindowClosed": true, + "frame": false, + "alwaysOnTop": true, + "resizable": false, + "defaultCentered": true, + "defaultHeight": 700, + "defaultWidth": 600, + "saveWindowState": false, + "showTaskbarIcon": false, + "cornerRounding": { + "width": 10, + "height": 10 + }, + "preloadScripts": [ + { + "url": "http://localhost:8080/preload/auth-preload-check.js" + } + ] + }, + "shortcut": { + "company": "OpenFin", + "description": "A way of showing examples of what OpenFin can do.", + "icon": "http://localhost:8080/favicon.ico", + "name": "Integrate Server Authentication - Hidden Provider - v19.1.0", + "target": ["desktop", "start-menu"] + }, + "supportInformation": { + "company": "OpenFin", + "product": "Workspace Starter - Integrate Server Authentication - Client - Hidden Provider", + "email": "support@openfin.co", + "forwardErrorReports": true + } +} diff --git a/how-to/integrate-server-authentication/server/src/routes.ts b/how-to/integrate-server-authentication/server/src/routes.ts index 31bf43cc15..c5bed26441 100644 --- a/how-to/integrate-server-authentication/server/src/routes.ts +++ b/how-to/integrate-server-authentication/server/src/routes.ts @@ -5,7 +5,8 @@ import path from "path"; const router = express.Router(); export default router; -const sessionIds: { [id: string]: unknown } = {}; +const sessionIds: { [id: string]: string } = {}; +const stuckUsers: { [id: string]: boolean } = {}; const SESSION_COOKIE_NAME = "app-session-id"; /** @@ -19,6 +20,17 @@ function corsMiddleware(req: Request, res: Response, next: NextFunction): void { next(); } +/** + * Clears the session dictionary of the given session id. + * @param sessionId The session id to clear. + */ +function clearSessionDictionary(sessionId: string): void { + if (sessionId) { + delete stuckUsers[sessionId]; + delete sessionIds[sessionId]; + } +} + /** * Get the platform provider.html, if the user has no authentication cookie * then instead redirect to the login route. @@ -26,14 +38,37 @@ function corsMiddleware(req: Request, res: Response, next: NextFunction): void { router.get("/platform/provider.html", (req, res, next) => { console.log("Received request for /platform/provider.html"); if (req.cookies?.[SESSION_COOKIE_NAME] && sessionIds[req.cookies[SESSION_COOKIE_NAME]]) { - console.log("Session cookie available. Navigating to /platform/provider.html"); - res.sendFile(path.join(__dirname, "..", "..", "public/platform/provider.html")); + const sessionId = sessionIds[req.cookies[SESSION_COOKIE_NAME]]; + const isStuckUser = stuckUsers[sessionId]; + if (isStuckUser) { + console.log("Stuck user detected. Navigating to /app/stuck"); + res.redirect("/app/stuck"); + } else { + console.log("Session cookie available. Navigating to /platform/provider.html"); + res.sendFile(path.join(__dirname, "..", "..", "public/platform/provider.html")); + } } else { console.log("Session cookie not available. Navigating to /app/login"); res.redirect("/app/login?return=/platform/provider.html"); } }); +/** + * The stuck page represents a page that is intended to be a redirect where encountered an error and stopped. + */ +router.get("/app/stuck", (req, res, next) => { + console.log("Received request for /app/stuck"); + res.sendFile(path.join(__dirname, "..", "..", "public/app/stuck.html")); +}); + +/** + * The friendly error page is something shown to users if something has gone wrong e.g. they are stuck before the provider is loaded. + */ +router.get("/app/friendly-error", (req, res, next) => { + console.log("Received request for /app/friendly-error"); + res.sendFile(path.join(__dirname, "..", "..", "public/app/friendly-error.html")); +}); + /** * Get the app.html content, if not authentication redirect to the login page. */ @@ -73,7 +108,8 @@ router.get("/app/login", async (req, res, next) => { console.log("Received request for /app/login"); if (req.cookies?.[SESSION_COOKIE_NAME]) { console.log("Session cookies available so clearing them as login has been requested."); - delete sessionIds[req.cookies[SESSION_COOKIE_NAME]]; + const sessionId = sessionIds[req.cookies[SESSION_COOKIE_NAME]]; + clearSessionDictionary(sessionId); } res.clearCookie(SESSION_COOKIE_NAME); console.log("Navigating to /app/login.html"); @@ -88,7 +124,8 @@ router.post("/app/logout", (req, res, next) => { console.log("Received request for /app/logout"); if (req.cookies?.[SESSION_COOKIE_NAME]) { console.log("Session cookies available so clearing them as login has been requested."); - delete sessionIds[req.cookies[SESSION_COOKIE_NAME]]; + const sessionId = sessionIds[req.cookies[SESSION_COOKIE_NAME]]; + clearSessionDictionary(sessionId); } res.clearCookie(SESSION_COOKIE_NAME); console.log("Navigating to /app/login.html"); @@ -101,7 +138,7 @@ router.post("/app/logout", (req, res, next) => { router.post("/app/login", (req, res, next) => { console.log("Received post to /app/login.html"); if (req.body?.email === "test@example.com" && req.body?.password === "pass1234") { - const sessionId = Date.now(); + const sessionId = `${Date.now()}`; sessionIds[sessionId] = sessionId; res.cookie(SESSION_COOKIE_NAME, sessionId); const returnUrl = req.body?.returnUrl as string; @@ -109,10 +146,22 @@ router.post("/app/login", (req, res, next) => { `Login details test@example.com / pass1234 session cookies set. Redirecting to originally requested url: ${returnUrl}` ); res.redirect(returnUrl); + } else if (req.body?.email === "stuck@example.com" && req.body?.password === "pass1234") { + const sessionId = `${Date.now()}`; + sessionIds[sessionId] = sessionId; + stuckUsers[sessionId] = true; + res.cookie(SESSION_COOKIE_NAME, sessionId); + const returnUrl = req.body?.returnUrl as string; + console.log( + `Login details stuck@example.com / pass1234 session cookies set. Redirecting to stuck url: /stuck instead of ${returnUrl}` + ); + res.redirect("/app/stuck"); } else { console.log( - "Demo credentials not provided. You need username/password of test@example.com / pass1234 to login" + "Demo credentials not provided. You need username/password of test@example.com / pass1234 to login or stuck@example.com / pass1234 to get authenticated and stuck." ); + const sessionId = sessionIds[req.cookies[SESSION_COOKIE_NAME]]; + clearSessionDictionary(sessionId); res.clearCookie(SESSION_COOKIE_NAME); res.status(401).send("Unauthorized"); }