-
Notifications
You must be signed in to change notification settings - Fork 265
Front End Development
#Front end code Held together by glue and duct tape. There are several distinct areas - the dialog, the main site, authenticate_with_idp, and the communication_iframe.
##Dialog
###Dialog Startup
start.js registers and loads a bunch of PageModules. The most important module is dialog.js. Dialog.js waits for a call to its get
function. When get is called, it means communication channels have been established with either WinChan or a native implementation. At this point, the state machine is started up and the "start" state is called.
###Dialog Modules (controllers)
Modules generally take care of an individual screen, but they can be generic. Modules are located in the controllers directory. A Module is an item that can be registered, started, or stopped at any time. All items in the controllers directory are PageModules - which are Modules with special DOM functionality.
dialog.js
is a special module in that it does not take care of any individual screen but is the entry point for WinChan and native implementations to start the dialog's state machine in motion.
###Interplay between Mediator, State Machine, Actions and Modules. There is a lot of inter-play between the Mediator, State Machine, the Actions module and Modules. The basic idea is that the state machine keeps track of which state the user is currently at and where it should go, depending on user input. Actions is to keep the state machine clean - it is responsible for starting up the modules or controllers responsible for handling a particular state.
When a Module finishes it's lifespan, it triggers a message onto the Mediator (a PubSub mechanism). The State Machine listens for messages on the Mediator and takes appropriate action. The State Machine will then call an action within the Actions script and the cycle will repeat itself.
Words make this difficult to understand. A picture (with a lot of words) is far better. Start at #1 (State Machine)
###Diagram of User Flow through the Dialog Displayed is a diagram of the various stages of user interaction. This flow takes the user from checking the users authentication, through the authentication page, and off to some other screens depending on user input. This diagram shows how the pieces fit together using concrete pieces of the dialog.
A more complete (though higher level) view can be seen in the next diagram.
##Main Site ###Main Site Page Controllers
the /pages directory is a bit like the wild west. Some of these are leftovers from when Bryan and Andy started the mockups. Some have been re-written. Some are based on the PageModule concept described above, but work to convert all of these has not been completed. The correct "controller" is started in the start.js script when it examples document.location.href.
It is fairly straight forward to add new Key Performance Indicators (KPIs). KPIs are performance measurements that are taken during a dialog session. The data consists of a list of important values such as number of emails a user has as well as an event stream. The event stream is a timed set of events that occur while the dialog is open. Knowing these bits of information allow us to make informed decisions as to what works and what does not.
The entire dialog is event driven. The Message Hub broadcasts all messages out to anybody who is listening. Messages are captured in resources/static/shared/modules/interaction_data.js. Within this file is a table named MediatorToKPINameTable which translates message hub names to KPI names. All event KPIs that are captured are listed in this table.
Any new non-event KPI must first be added to the whitelist of KPIs that are sent to the server. The whitelist is located in resources/static/shared/models/interaction_data.js, called KPI_WHITELIST. Once the KPI is added to the whitelist, the "kpi_data" message can be triggered on the mediator with the values to be added. Here is an example from resources/static/dialog/state.js:
mediator.publish("kpi_data", {
number_emails: storage.getEmailCount() || 0,
sites_signed_in: storage.loggedInCount() || 0,
sites_visited: storage.site.count() || 0,
orphaned: !self.success
});
All non-event KPIs that are sent to the server are listed in the KPI_WHITELIST. If the "kpi_data" message is triggered with a KPI that is not in the whitelist, it will not be sent to the server.
A fair amount of data is persisted on the client. Data in siteInfo
, emails
, managePage
, and returnTo
is cleared whenever a user's session expires, other fields are stored until the user clears their cookies.
-
emailToUserID
- dictionary of email addresses to user ids. Used in conjunction with usersComputer to keep track of whether a user answered yes or no on the "Is this your computer?" screen. -
interaction_data
- data stored to send to the KPI system. -
managePage
- keep track of whether the user has visited the manage page. -
returnTo
- the URL to return a user to after verifying an email address. Set every time a user must verify an email address. -
usersComputer
- used in conjunction with emailToUserID to keep track of whether a user answered yes or no on the "Is this your computer?" screen. -
emails
- a dictionary that holds the public & private keypair and generated certificate. Keyed by email address. -
siteInfo
- a dictionary that contains site specific info used to sign users into a site. Keyed on RP domain. Sub-fields areemail
,signed_in
andissuer
. -
siteInfo.email
- email address last used to sign into domain. This field causes the email address to be pre-selected in the dialog. -
siteInfo.signed_in
- A boolean that indicates whether the user is signed into the domain. If true, silent assertions can be generated when an RP calls navigator.id.watch. If false, assertions are not generated but the email address specified inemail
will still be preselected when the user opens the dialog. -
siteInfo.issuer
- the issuer that generated the last assertion. Used in FirefoxOS to indicate whether the last assertion was generated to sign into Marketplace or into a normal site. If used to sign into Marketplace, the issuer will befirefoxos.persona.org
.
To cover legal bases, Persona must present the user with two sets of Terms of Service and Privacy Policies. The first set is for Persona itself, the second set is for the relying party. If an RP specifies both termsOfService and privacyPolicy, the ideal is to present them users who have never previously visited the site. A user's record of visiting a particular site is only kept on the client side in localStorage and since localStorage is cleared whenever a user's session expires, the best we can do is approximate on a per-device basis. An RP's TOS/PP agreements will be shown in 3 areas - 1) in the authenticate dialog if a user is not authenticated, 2) in the pick_email dialog if localStorage shows no record of the user visiting the site, or 3) in requiredEmail if localStorage shows no record of the user visiting the site. requiredEmail has two corner cases where TOS/PP are set in other controllers - a new user who is immediately redirected to set_password, and a user using a primary email who has not visited a site before and is redirected to verify_primary_user.
Persona's TOS/PP are shown in 5 places - on the signup page on the main site, to all unauthenticated users in the authenticate controller, and for the requiredEmail case in set_password, required_email, and verify_primary_user.
We have an internal API for use by native iOS and Android apps like Pancake and Soup. Since native apps do not open the dialog using the functionality provided by include.js, they must have alternate mechanisms to get assertions. The internal api is the alternate mechanism and is defined in resources/static/dialog/resources/internal_api.js
.
Using the API has one key dependency - the dialog has to know BrowserID is running within a native context.
To do so, the native app has two choices.
- Define navigator.id.channel.registerController
- Append either "#NATIVE" or "#INTERNAL" to the embeded browser's URL.
These must be done before the BrowserID code is run.
navigator.id.channel.registerController can be defined with the following snippet which must be included before the Persona Javascript is downloaded.
navigator.id = navigator.id || {};
navigator.id.channel = navigator.id.channel || {};
navigator.id.channel.registerController = function(controller) {};
https://login.persona.org/sign_in#NATIVE
or
https://login.persona.org/sign_in#INTERNAL
BrowserID.internal.get(<origin>, function(assertion) { /* pass assertion back to native app */ },
options);
- get(origin, callback, options) - Get an assertion. Mimics the behavior of navigator.id.get
- logout(origin, callback) - logout of an origin
- logoutEverywhere(callback) - logout of all sites
There are several important gotchas when using the Persona dialog within a native app.
- navigator.id.channel.registerController must be defined before Persona Javascript is run.
- The app must set the origin to a valid http or https origin.
- Persona makes use of localStorage and it is likely the localStorage used in the native app is sandboxed from the native browser's localStorage.
session_context gives the dialog (and main site) a bit of info that it needs to operate. It gives the user's authentication status, a CSRF token and a server time.
-
csrf_token
- a CSRF token used for all POST requests. -
server_time
- the server time in milliseconds, used to generate assertions. -
authenticated
- true or false. -
auth_level
- if authenticated is true, this can be "password" or "assertion". -
domain_key_creation_time
- the last time the main super secret key for Persona was changed. If a client side certificate has a create date older than the domain_key_creation_time, it is invalidated and a new certificate must be generated. -
random_seed
- a cryptographically random string sent by the server to help generate client side keys -
data_sample_rate
- a number between 0 and 1 representing the percentage of users who should have KPI data collected. -
userid
- the user's id.
auth_level
mentioned in the session_context information is used to keep track of a user's authentication level. BrowserID has a notion of two types of identity providers, they are primaries and secondaries. A primary identity provider is an identity provider that supports the BrowserID protocol and can vouch for its users identities directly. A secondary identity provider is a fallback identity provider that verifies email addresses for domains which do not support the BrowserID protocol. Secondary identity providers are meant as a bootstrapping mechanism and can in theory go away once all email providers support the BrowserID protocol. At the moment, Mozilla Persona is the only secondary identity provider.
Users who only sign in to Persona using email addresses that support the BrowserID primary protocol will have no Persona password. These users will only ever authenticate with their email providers. Users who add an email address which must be verified using the secondary protocol must create a Persona password.
Because of these differences, there are two corresponding "authenticated" authentication levels. A user who signs in to Persona using a primary identity provider is authenticated to the "assertion" authentication level. A user who signs in to Persona using a secondary email address is authenticated to the "password" level.
A user who is authenticated to the "assertion" level can see all of the other email addresses available under that account, but they may have to go through additional authentication to select a different address. If a user authenticated to the "assertion" auth_level selects a secondary address, they must enter their Persona password to be able to use that address.
A user who selects a secondary address and enters their Persona password will be authenticated to the "password" level. These users can select other secondary addresses without entering an additional password, but if they select a primary address, they may have to authenticate with their email provider.
These distinctions are important to the front end because the UX is slightly different between the two.
Local testing of the primary identity provider flow can be done by entering an @example.domain address into the email field.
##Context Interaction Between the RP, dialog and communication_iframe The RP embeds Persona's include.js into their site. include.js provides the RP facing API and bootstrap code that loads both the communication_iframe and the dialog.
All communication between the RP and the other contexts occurs via abstraction layers and ultimately window.postMessage. These abstraction layers are used to smooth over the differences in browser support for window.postMessage. JSChannel is used between the RP and the communication_iframe. WinChan is used between the RP and the dialog.
Whenever navigator.id.watch is called on an RP, an iframe called the communication_iframe is embedded in the RP's page. communication_iframe takes care of checking the user's authentication status, syncing email addresses with the Persona backend, and finally, generating an assertion if the following conditions are true:
- The current user is authenticated to Persona.
- The user has visited this site before, selected an email address, and the association is remembered.
- The remembered email has a valid certificate.
- The dialog is not open.
If the conditions are not met when the iframe first loads, the communication_iframe continues watching to see the conditions are met at any point in its lifespan. As soon as all of the conditions are met, an assertion will be generated and handed off to the RP. This can happen whenever a user creates a new Persona account from one tab, closes the Persona dialog then verifies their email status in a second browser or tab.
The dialog is opened whenever navigator.id.request is called on the RP. If the dialog is open, it will take care of generating the assertion and passing it back to the RP via postMessage.
The front end has a huge number of QUnit based unit tests. Even so, test coverage is not complete. Most controllers, pages, and items in shared are to a large extent covered, but resources/state.js and resources/actions.js are two notable exceptions. When new functionality is added, new tests should be written to cover edge cases, boundaries, expected failure modes, as well as the happy path. Front end tests can be run from the command line or in the browser. Pull requests that change functionality or add new features will not be accepted unless accompanied by unit tests. Any pull request with a failing test will not be accepted.
From the root BrowserID directory, tests can be started by typing:
./scripts/test_frontend
If a server is running, tests can be run from the browser by pointing the browser to:
http://<host>:10002/test/index.html
A subset of tests can be run in the browser by appending a ?filter=<filter>
on to the end of the URL. For example, to run the tests for shared/user.js:
http://<host>:10002/test/index.html?filter=shared/user
Backend tests can be run only from the command line. These can be run by entering the root BrowserID directory and typing the following:
./scripts/test_backend
If MySQL is installed and set up for BrowserID, the backend unit tests can be configured to use the MySQL database driver:
NODE_ENV=test_mysql ./scripts/test_backend
##General Rules to Follow when Coding
- Pages or Modules can only talk to either their own
helpers
objects or items inshared
. In reality, this means Modules and Pages normally only interface with their helpers, Users, Network, Storage and the Mediator. - Modules never interface directly with the State Machine or Actions. Modules are pretty unaware of the world around them (purposefully), they send messages to the Mediator which will then be picked up by the State Machine.
- Items in
shared
cannot interface directly with either Pages or Modules. - Anybody can trigger a message on the Mediator. Just because a message is triggered does not mean there is a listener for it.
- When a Module has terminated its normal flow, it should NOT trigger a message saying "go to this state". It SHOULD instead trigger a message saying "this is what just happened and these are the input". The State Machine will take care of deciding which state to go to. This is so user flows can be modified and tested from one central location.
This section has grown beyond the scope of this document and is now found at https://github.com/mozilla/browserid/wiki/Developing,-Pull-Requests,-Code-Reviews-and-Merging. [missing July 2013?]
##Intro Exercises for the Reader
-
Modify State Machine Order (dialog) - UX testing has decided it would be better for the users to go to the email picker screen after entering their BrowserID username and password (this was only recently changed to skip the email picker). How would you do this? If a hint is needed, see https://github.com/mozilla/browserid/commit/23de3284c55a50a3241227815fc00eb115183dce
-
Add A New Screen (dialog) - UX testing has confirmed that users are more comfortable on user sign up setting their password in the dialog instead of on the main site confirmation page. How would you insert a screen to allow the users to set their password once you know that an account is new?
Desired flow: On authentication screen, user enters email address and clicks "next". BrowserID checks email address and discovers it is new. User is then directed to a set password screen where they can set their password. Once user enters password, they are directed to the "wait for email verification" screen. If a hint is needed, see https://github.com/mozilla/browserid/tree/usertest_1000_in_dialog_password
Note: Because there is not yet backend support from this, no backend calls or saving of the password need to be done. -
Add Emails (main site) - Users want the ability to add emails from the manage emails page. As a stop gap measure it has been decided to use the "required email" flow of the dialog with an input field in the manage screen. How would you do this? (this has never been implemented, so no hints to look at)
All client side code lives in resources/static
resources/static
/auth_with_idp - used when returning from authenticating with IdP
/communication_iframe - silent assertion IFRAME
/common - Files common to both the main site and dialog.
/css - Common CSS
/js - Common JS - contains much of the core functionality
/lib - 3rd party libraries
/models - data models
/modules - shared modules/views/controllers
/dialog - code related to dialog
/css - dialog specific CSS
/js - dialog specific JS
/modules - Controllers generally handle screens, not all do.
/dialog.js - The entry point which starts the state machine.
/misc - Misc helper type functions
/state.js - The STATE MACHINE!
/views - dialog templates
/i - dialog specific images
/start.js - the dialog start script.
/pages - main site pages
/css - main site specific CSS
/js - main site specific JS
/i - main site specific images
/relay - relay frame used by WinChan for IE8
/test - unit tests
- Server Side Templates - resources/views
- Client Side Templates - resources/static/dialog/views