Single sign in or how to share data in your browser across domains
Every modern browser offers mechanisms to store data that can be retrieved when opening a new window or a new tab. The different storage options are cookies, local storage, and session storage. The problem is that these storage options are segmented per domain (or subdomain). That means that xyz.com will not be allowed to access any of the information that abc.com has set. And browsers don’t provide any options to share data across domains, for obvious security and data privacy reasons.
At Bloom Partners, we build digital solutions that drive long-term consumer engagement through content delivery. This often requires our users to use several web apps, that connect to the same authentication API. Those web-apps may not use the same framework, as the purpose of the app drives what platform it is based on.
Considering that a secure API only allows a few active authentication tokens, if your user needs to use more apps than the number of available tokens allowed by the API, he will not be able to stay logged in all the apps he needs to use. Therefore he will need to go through the authentication process multiple times a day.
The article is a step by step guide on a solution we built at Bloom Partners to answer this problem in a secure, framework-agnostic manner, using Webpack and Typescript only.
Overview of the solution
The previously mentioned snippet will also register a few methods (set, retrieve, delete) on the window object, adding a layer between the iframe and your application to streamline the communication between them.
The compiled result will be composed of
- A “snippet.js” script, registering the iframe on the index.html, and providing the methods to the window object. The methods provided to the window object should return promises, that will be resolved or rejected when the snippet receives specific events from the iframe.
- An index.html, that will import its script: “iframe.js”. This script will listen to the events coming from the snippet and send events in response.
The communication between the script and the iframe will be done through the window.postMessage interface.
As a good picture is worth a thousand words, here is a time diagram showing the path of an application requesting an authentication token to the sign in script:
Step by step guide
Let’s go deeper in the details of the implementation. If at any point you are lost, you can check up the finished version of this in the following repository: Github repository
1. Project setup and minimal Webpack configuration
The first step is to create a new directory, initialize NPM (“npm init”), and create our file structure, which will be composed of:
A “src/” folder containing 3 folders:
- iframe/, that will contain the code related to the iframe: We can already initialize it with an index.html file and an “index.ts” file referenced in the index.html.
- snippet/ that will contain the code related to the single sign-in initialization (spawning the iframe on the page), and the code related to the snippet itself. We can already initialize it with an “index.ts” file.
- A shared folder containing our models, Enums, and any piece of code that needs to be shared between the 2 elements
As you’ve seen in the previous step, we created some typescript files, so we need to configure typescript adding a basic “tsconfig.json” (see Here link or preview to the tsconfig.json file)
It is now time to build the Webpack configuration. We are going to need the following Webpack plugins:
- HtmlWebpackPlugin: generates the HTML file of the iframe. We will pass it as argument the template (index.html file previously created), and add the “excludeChunks” parameter, to exclude the snippet’s script
- ForkTsCheckerWebpackPlugin to speed up the type check
We also need to specify our entries and outputs: We have 2 independent entries: “src/iframe/index.ts”, and “src/snippet/index.ts”. And we want those 2 entries to be compiled in 2 files, so the output parameter of the Webpack configuration should look like:
path: path.resolve(__dirname + ‘/dist’)
And the last step is to create two NPM tasks in the package.json, one that builds the project for production purposes (minified and optimized), and another that serves it in a development server.
In order to have a running development server, we will use webpack-dev-server
After this step, your repository should look like the following Github repository: Project setup
Everything compiles properly and running “npm run build” builds the 3 files that we need: snippet.js, iframe.js, and index.html, referencing our iframe.js file. However, there is no logic in those files.
2. Spawn the iframe on the page
The first thing we need to do is to spawn the iframe on the page, during the initialization process of the snippet.js. For this, we’ll create an “iframeUtils” class in a separate file in the snippet folder that contains a “createIframe” and a “getIframe” method.
The “createIframe” function creates the HTML element using the document.createElement web API sets its source and id, and a couple of styling elements to make sure that it is not visible on the page. Once this is done, it appends it to the document’s body using the appendChild method.
And we will call this “createIframe” method from the main function of the index.ts file.
After this step, your repository should look like the following Github repository
We now have a basic Webpack configuration and a script that spawns the iframe on the app’s index.html file. It is now time to add the business logic.
3. Registration of events in the iframe
Like the “snippet.ts“ file, the “iframe.ts” file contains a “main” function that runs on load. This method registers the different event listeners, with a callback.
The iframe will listen to three different events, with a self-explanatory name: GET_AUTH_TOKEN, STORE_AUTH_TOKEN (with the authentication token in the parameters), and DELETE_AUTH_TOKEN. and will send an event to its parent window with the response.
You can see in the last commit of this part, that I added a few models to properly type the message event data, and the “snippet-events.enum.ts”, that defines all the events that the snippet can send. Those events are the ones that we will be listening to in the iframe.
After this step, your repository should look like the following Github repository.
4. Registration of the methods in the window element in the snippet
The goal of this part is to provide the different methods to create, retrieve, and delete an authentication token, so that an application that uses it can simply call “window.singleSignIn.getAuthToken()” and receive a promise as response.
However, in a Typescript environment, the window interface is set and does not contain “singleSignIn” element part of its data model. We therefore need to overwrite this window interface, declaring it globally. You can check the following stack overflow link for more information on how to extend the window object data model.
Let’s now create a “MethodsService” class, that contains a method to register the functions on the window object. It contains the 3 methods that we want to attach to the window element, and a function to register them on the window object. The last step is now to call this registration function from the main function in the “index.ts” file in the snippet’s repository.
After this step, your repository should be like the following Github repository.
5. Send events to the iframe from the snippet and answer in the iframe’s script
We have the frame for the communication between the iframe and the snippet, it is now time to send messages. In that part, we will fill all the functions in the “MethodsService” and the “EventListener” service.
In the method service (snippet), when one of the window methods is called, we register a one-time listener that will listen for one of the 2 outputs that the iframe can send: Positive or negative response: For example, in the case of “getAuthToken”, we will react if the message sent by the iframe is “GET_AUTH_TOKEN_RESPONSE” or “GET_AUTH_TOKEN_ERROR”. once the listener is registered, we will send to the iframe the event named “GET_AUTH_TOKEN” and return a promise. The promise is either resolved or rejected in the handler method.
In the EventListenerService, we access the local storage and we simply answer to the snippet using “window.parent.postMessage()”
After this step, your repository should be like the following: Github repository
6. Implementation of a demo HTML file that would use the snippet the same way you could use it in an app
The whole business logic is now done, so let’s create a static index.html file at the root of our repository that uses our snippet. This file only contains 2 script tags:
- The first one simply imports our snippet.js file
- The second one tries to store, retrieve, and delete an authentication token.
We do not need to serve that file through a Webserver, we can simply open it in the browser from the file system.
It is however necessary to have the single sign-in webserver running, as the script is imported through your localhost.
7. Secure the connection between the frame and the snippet
We now have a fully functional single sign in handling. However, we don’t want any external pages to be able to use it, or any malicious script that would try to get the authentication information of our platform. To avoid that, the iframe is going to check the domain of the parent page and compare it with a list of white-labeled domains stored in its code. If the parent page is not from an authorized domain, we don’t set up the event listeners on the iframe side. That implies that the requests from the snippet will simply be ignored, and the promises never fulfilled. Therefore, the malicious website cannot access the information that we stored.
You can check the final code here
Conclusion and potential improvements
We went through the different steps needed to create a single sign-in application. Now, if you have two web apps importing the snippet script, they can both access the same browser storage and thus share an authentication token. The user only needs to sign in one application, and he gets redirected to the authenticated area in all the other apps. All the apps use the same authentication token, which therefore has no chance to get invalidated.
However, this solution still misses a few key points in order to be production-ready:
First, we should set up a linter like “tslint”, if multiple people start to work on the project. It would also be good to add a testing framework, and tests to improve the robustness of the solution, and avoid regressions when adding new features. But I did not add this part in the tutorial to avoid adding extra complexity.
In the core features, another improvement could be added:
For now, we don’t really know when the iframe and snippets finished loading. So, we need to wait a little while to be sure that both are loaded. A good improvement would be to set up a “queue” mechanism in the snippet and to register the methods to the window object at the very beginning. We could then avoid having to wait in the app to do the first request.
Finally, we only support the storing n authentication token as a string format. Some use cases may require you to share some more complex data. This is not an issue: You could easily create some more methods and events to do so.
If you have any comments, suggestions, or questions, don’t hesitate to drop a comment! I’d be happy to answer and improve the solution!