What changed in the login system?
Previously, by clicking in the login button of the LFG, for instance, the user was redirected to the Accounts to start a new session if he/she did not had one already, and then he/she was redirected back to the LFG with an access token in the URL that the LFG stored and used to start the its own session and to request user information to the Accounts.
This flow, however, has 2 main problems:
- as each app is responsible for starting and destroying their own sessions, apps do not share login with each other: by logging into the LFG, you will not be automatically logged in to Donate and vice versa
- all applications are required to have a similar logic for storing their session token, which if changed would need to be updated individually in each app
The implemented solution for these problems is not the most beautiful, but it was the best I have found so far.
Now, the responsibility of starting and destroying sessions in all RPGist apps no longer belongs to individual apps, but to Accounts itself: by logging into the Accounts, it writes the session cookie into each of the integrated apps before redirecting the user to back to the app that requested the login. Likewise, when the user clicks on a sign out link of any app, he/she is redirected to the Accounts, that clears the session cookie from all apps and then terminates its own session.
The difficulty part is that each app exists in its own domain and, for security reasons, browsers do not allow sites in different domains to read or write cookies from websites in other domains.
Since all RPGist apps are in rpgist.net subdomains, it would indeed be possible to write a shared cookie, but this solution would not work in a development environment where apps run on
http://localhost, each in its own port.
How can Accounts write the access token in the other apps then?
Just as before, by clicking in the login button for any app, such as the LFG for example, the user is redirected to Accounts, where he/she should start a session if he/she does not have one already. Now, on the other hand, instead of being immediately redirected back, the user is directed to a login loading page inside Accounts where it loads an invisible
<iframe /> for each integrated app, within which it loads their respective pages responsible for signing the user in the app.
The same thing happens during the sign out process: the user is redirected to the Accounts, that loads the pages responsible for destroying the sessions of each app, and then ends its own session.
This solves the problem 1. By logging into an app, you're logged in at all of them. By signing out of any app, you are signed out of all of them.
To solve problem 2, the solution was even more creative.
The page responsible for starting and destroying sessions that exist in each app, which is loaded in the
<iframe /> mentioned above, is a static HTML stored in the public folder of each app. An identical copy for each one. If the logic for creating and destroying sessions stayed in the HTML itself, however, the problem would persists: if anything changes in the session management logic, I would need to copy and paste the updated HTML into each of the apps and deploy all of them. Not very DRY.
The responsibility of this HTML, therefore, is only to load a script, stored in the Accounts that itself contains the logic for creating and destroying sessions. The logic, therefore, is centralized in one place, which is in the service responsible for managing users and sessions.
To make it even easier to maintain, this script loads dynamically, just as Google Analytics and the Facebook SDK do, for example, and from an URL it receives via query params. In this way, the HTML does not even need to store the address of the Accounts' script, which, by the way, changes every time the script is updated due to suffixes that Sprockets places in the names of the files it precompiles.
Finally, in order to not leave the file vulnerable to receiving and running malicious scripts, the script is only loaded if the received URL is under the RPGist domain or localhost.
In summary, the flow is as follows:
- the user clicks on the login link of any app and is redirected to Accounts passing the app ID and a redirect URL as query params
- Accounts validates the app ID and redirect URL received and, if they do not match its whitelist, it redirects the user back, without logging him/her in
- if the ID and URL are valid, the user is presented to the Accounts login and registration page
- after registering and/or logging in, the user is redirected to an Accounts page that loads the
<iframe />for the integrated apps and shows an animated loading icon while logins are performed
- in the
<iframe />are present the access token name (to be used as key when writing the cookie), the access token itself, and the URL of the Accounts script responsible for writing the token in cookies of the app
- the HTML loaded inside the
<iframe />validates if the received URL belongs to the RPGist domain and loads the scritp dynamically
- the loaded script reads the name and value of the access token and writes the token in the app's cookies
- Accounts listens to the loading event of each
<iframe />and when all of them are finished loading, it redirects the user back to the redirect URL that he received in step 1
Voilà! Single Sign On!
PS: If anyone has any suggestion on how to improve this even further, feel free to leave them in the comments below or call me on the chat of our Facebook page, or you can send me an email at firstname.lastname@example.org. Appreciate!