Content-server architecture
Current as of Jan 20th, 2020
Document purpose
This is a starting point for developers and contributors of the Mozilla accounts Content Server. The content-server is responsible for displaying the authentication/authorization and account settings related UI to the user.
Helpful to know before starting
At a high level, FxA is an authentication (authn) and authorization (authz) system. This means a Relying Party (RP) such as Firefox Sync delegates the responsibility for authenticating and authorizing users to Mozilla accounts. Once the user proves their identity to FxA, we return one or more tokens the RP can use to access a "protected resource". The OAuth 2.0 spec does a good job of explaining these concepts.
While FxA is an OAuth 2.0 authorization server, OAuth is only one of several integration types supported by FxA. For more information about how FxA came to support so many non-standard integrations, see the App Services and FxA Unofficial History.
Detailed Maps of the FxA Universe outline how the various Mozilla accounts micro-services fit together.
Relying Party Integrations
The content server architecture was heavily influenced by the number of ways Relying Parties (RPs) integrate with FxA. If portions of the architecture seem byzantine and complex, chances are this is why.
- 4 browser environments for Firefox Sync:
- Firefox Desktop
- Firefox for Android (Fennec)
- Firefox for Android (Fenix)
- Firefox for iOS
- WebExtensions such as Notes and the Firefox Private Network "secure proxy" (FPN)
- OAuth 2.0 such as Monitor
- Native applications such as the Firefox Private Network VPN
- "Direct access", i.e., users browsing directly to https://accounts.firefox.com
Content server architecture basics
The content server is comprised of a server and client side components.
The server side is responsible for serving static content and ingesting metrics and error reports.
The client side is a Backbone based Single Page Application (SPA) and responsible for screen rendering, handling user input, communicating with various FxA backend services, and communicating with RPs or browsers. The client app runs in modern Web capable systems, see supported browsers for up to date tier 1 support. Our use of Backbone isn't strictly Model-View-Controller (MVC), ours is more like MVVM. We use Models and Views and have models for the Views.
We're in the middle of a conversion to React which will obsolete Backbone. Read more details about Phase II.
High Level Web Client Components
Auth-Brokers
Auth-Brokers have two primary responsibilities:
- Mediate communication between Mozilla accounts and the RP
- Screen->screen state machine
To minimize the complexity, each integration type has its own broker so that customizations can be made without interfering with other brokers.
Brokers are made up of a list of capabilities, methods, and behaviors. Brokers can extend exisiting brokers and overwrite any of these properites to meet the specification of that integration.
- capabilities can be thought of as feature flags, e.g., "does this integration support signup?" (some versions of Firefox for iOS do not)
- methods are invoked by views before and after certain events happen, e.g.,
afterSignIn
is called after a user signs in and can be used to notify the RP. Methods usually return a "behavior", i.e., "what to do next". - behaviors define "what to do next" after a method has completed. For example,
afterSignIn
by default sends the user to the /signin_confirmed screen.
Brokers communicate with RPs via the URL or a Channel.
While placing screen->screen transition logic in auth_brokers might seem like an odd choice, this resulted in a huge simplification over when complex transition logic was embedded in views. Some screen->screen transitions depend on the integration type, and embedding this logic within the Views resulted in an unmaintainable mess. The work to completely remove transition logic from views was never completed, and we have since made many transitions more universal and it may be that we could move some of this logic back into views. It may also be that distinct state machines outside of brokers would have been a better choice.
Auth Broker implementations
Broker | Description |
---|---|
base | Base broker that other brokers inhiert from. |
fx-desktop-v3 | v3 of the Fx Desktop authentication broker, it allows the uid of a user to change if the user's account has been deleted. |
fx-fennec-v1 | Interfaces with Firefox for Android when authenticating for Sync, it communicates with the browser using WebChannels. |
fx-ios-v1 | Interfaces with Sync on Fx for iOS. Uses the same custom protocol as fx-desktop-v1. |
fx-sync | Generic broker used to integrate into Sync. This is used as a base class. |
fx-sync-channel | Base broker that communicates with the Firefox browser. |
fx-sync-web-channel | Variant of Sync broker that communicates via webchannels. |
oauth-redirect | Oauth broker that finishes oauth flow by redirecting the current window. |
oauth-redirect-chrome-android | Created because Chrome for Android will not allow the page to redirect unless its the result of a user action such as a click. |
oauth-webchannel-v1 | WebChannel OAuth broker that speaks 'v1' of the protocol. |
web | Auth broker to handle users who browse directly to the site. |
pairing/authority | Manages the OAuth flow by WebChannel messages to the browser to help with a pairing-based flow. |
pairing/supplicant | SupplicantBroker extends OAuthRedirectBroker to provide a redirect behaviour as an OAuth flow. |
pairing/supplicant-webchannel | SupplicantWebChannelBroker extends OAuthWebChannelBroker to provide a WebChannel flow. |
Channels
A channel is a two way communication mechanism between a RP and Mozilla accounts. The method of communication is channel specific.
There are channels to:
- Communicate between FxA and the browser
- Communicate between FxA and a WebExtension
- Communicate between 2 or more FxA tabs
Reliers
A Relier fetches and holds data about the current RP. A Relier is created on startup and is a central place to ensure data coming into and out of FxA is safe and well-formed.
Three primary types of Relier models exist:
- browser (for Sync integrations)
- oauth
- web (called relier)
Any long lived data (e.g., email address, uid, client_id
), coming from an RP or on the URL
MUST BE validated and transformed within Reliers. While it seems natural to ingest and sanitize
data in the Views, we are unable to control what users and malicious actors do. Assuming users always
enter at /
, or at /complete_sign_up
, etc, does not hold. To prevent XSS, we would have to validate
and sanitize long lived data on every screen it is used, and we saw many cases in the past where we
forgot to do this. Ingesting and validating the data on startup in the Relier model ensures the data
is checked once and is ensured to be safe afterwards.
The front end could probably be significantly simplified if all query parameter validation logic was moved to the server.
User
A User holds and persists data about one or more Accounts as well as keeping track of the currently signed in user.
The user model was a premature solution to allow us to have an "account chooser" where a user visiting a new RP would be able to choose from any of their signed in Accounts. We never implemented this, and the User model has been a bit of a pain. The model does handle other things, though we could probably move most of its responsibilities elsewhere and simplify the overall architecture.
Account
An Account holds data about a given account and provides an API for making updates to the account. The account model uses the fxa-js-client, fxa-oauth-client, and fxa-profile-client to operate on the account in any way.
Router
The Router is responsible for reacting to URL changes and displaying the Views.
Views
Views represent either an entire screen or a portion of a screen. Views react to user input, and often call Broker methods to determine where the user should be sent after a successful form submission. Screen level views are rendered by the AppView. See View Deepdive for more info.
Templates
A template is a serialized HTML representation of a View. A view renders a template using data available to it and writes the rendered template to the DOM. Templates use the mustache template library.
Clients
Communication with external servers are done via client libraries.
fxa-js-client
The fxa-auth-client communicates with the Mozilla accounts Auth Server. The fxa-js-client is used for all aspects of authenticating a user - sign up, sign in, password reset, etc.
oauth-client
The oauth-client used to communicate with the Mozilla accounts OAuth Server and now communicates with the OAuth endpoints on the Auth server. The OAuth client is used to fetch OAuth codes and tokens to send to the RP.
profile-client
The profile-client communicates with the Mozilla accounts Profile Server. This client allows a user to interact with their profile data.
Application lifecycle
When the application starts, app-start.js takes care of setting up system-wide dependencies. app-start immediately determines the integration type and creates the appropriate Broker and Relier. The broker is queried to check support of the current integration. If the integration is supported, other models and the router are created. The router takes over and determines the initial View to display based on the browser's URL. The router creates the View, which in turn writes a template to the DOM. The user interacts with the View, either by filling out a form or clicking on links and buttons. A view can communicate with external servers using clients or via an Account model. Views usually invoke broker methods to determine next steps, which could be to redirect to another view, display a status message, or stop. Upon successful authentication with Mozilla accounts, the broker is notified, which in turn notifies a RP. The RP is responsible for the final fate of the Mozilla accounts tab. In the case of OAuth redirect, FxA redirects to the RP. In the case of Sync or a WebExtension, the tab may be closed automatically, or the user may have to close the tab themselves.
View Deepdive
View take care of all things UI. Each view has access to several models and libraries, the most commonly used are:
Even though React wasn't a thing, FxA views have their own lifecylce methods for rendering and form submission.
BaseView
BaseView is the parent of all Views. BaseView is responsible for rendering deciding whether to render a view, and if so, for rendering. BaseView also takes care of status messages, hooking up DOM events, l10n, and logging. BaseView does not take care of any form handling, for that, see FormView.
Rendering templates
setInitialContext
setInitialContext
is used to pass data to the Mustache template and is often overridden:
...
setInitialContext(context) {
context.set({
email: this.getAccount().get('email')
});
},
...
Render lifecycle methods
method name | purpose |
---|---|
beforeRender | Called before renderTemplate , can return a promise. If resolves to false , rendering is aborted, can be used to redirect a user to a different screen if pre-conditions are not met. |
renderTemplate | Renders the template with context. Rarely overridden. |
afterRender | Called after renderTemplate , can return a promise. If resolves to false , view is not displayed, can be used to redirect a user to a different screen if pre-conditions are not met. Often used by mixins to initialize based on elements in view.$el before being added to the DOM. |
afterVisible | Called after a screen has been added to the DOM. |
FormView
Extend from FormView for any Views that contain a form that can be submit. FormView takes care of several aspects of form submission:
- Ensure only one form submission can be in flight at a time
- Form validation
- Print submit error messages
Submit lifecycle methods
method name | purpose |
---|---|
isValidStart | Perform validation before individual input elements are validated. Return false to indicate form is invalid. |
isValidEnd | Perform validation after individual input elements are validated. Return false to indicate form is invalid. |
showValidationErrorsStart | Show validation errors before individual invalid input elements add their tooltips. If a truthy value is returned, no other validation errors are displayed. |
showValidationErrorsEnd | Show validation errors after individual invalid input elements add their tooltips. |
beforeSubmit | Called if form is valid, before submit . Can return a promise. Resolve to false to prevent submit from being called. |
submit | MUST BE OVERRIDDEN Do the form submission. Can return a promise. If promise rejects, the error will be displayed as an error message. |
afterSubmit | Called after the submit . Can be used for cleanup housekeeping. |
Showing validation errors
The showValidationError method is used to show validation errors on an individual input element, this is most commonly done in showValidationErrorsStart
or showValidationErrorsEnd
.
Mixins
The content server front-end relies heavily on Cocktail based mixins. Mixins enable code extraction into standalone modules that are "mixed into" other modules. Mixins have several purposes within FxA:
- Share functionality across modules without relying on inheritance.
- Isolate experiment code. If the experiment is deemed a failure, removing the experiment means removing the mixin rather than updating core View logic.
- Isolate related tasks to simplify reasoning.
Method name collision between a mixin and a mixed-in View is allowed and frequently used, understanding Cocktail's collision handling is important to avoid any surprises, especially relating to return values.
screen->screen navigation
this.navigate('confirm_signin', {
// fields here are passed to the next view
account: this.getAccount(),
});
The confirm_signin
view can access the data passed from the previous view:
const account = this.model.get('account');
More useful info
Adding a new route
- Create a new View module if needed
- Add a route entry to the frontend
- Add a route entry to the backend
- Add a test to ensure the new route responds with a 200
- Add some functional tests
Server errors and error messages
Any time a new error is added to the auth-server, a corresponding entry needs to be added to the content server to ensure the error text is translated.
Error messages can be customized depending if it makes sense for a particular context by setting the
forceMessage
property on an error before display. See this example.
Closing FxA and metrics (aka - missing metrics)
When redirecting to an external site, e.g., an OAuth RP, the content server makes its best attempt
at flushing metrics in an unload
handler. The problem with depending on unload
handlers
is this only works some of the time. The unload
handler is ignored on iOS, so we have to
ensure metrics are flushed before a redirect happens.
A common problem is setting navigator.location.href = url
from within view code without first
flushing metrics. Instead of setting location.href
, call this.navigateAway
which will ensure
A common problem is setting navigator.location.href = url
from within view code.
metrics are flushed. href
s from anchor elements are automatically handled by the
ExternalLinksMixin.
User data stored on device
Some user data is stored in localStorage to smooth out repeated auth requests and ensure consistent experiment choices across users.
All FxA related information is keyed with a __fxa_storage.
prefix.
Account data of all users is located under __fxa_storage.accounts
. The entry
is an object where the keys are account UIDs and the values are serialized account
data.
In environments that support the fxaccounts:fxa_status
WebChannel message,
account data stored in localStorage is merged with the signedInUser
field
of the fxaccounts:fxa_status
response.
Keeping tabs synchronized
If multiple FxA tabs are open, we try to keep tab signin state synchronized as much as possible. Whenever the user signs in or out, BroadcastChannel messages are sent to all other FxA tabs. Whenever another tab receives a message, it responds appropriately.