Skip to content
This repository was archived by the owner on Nov 1, 2018. It is now read-only.

oauth design

Brian Warner edited this page Mar 6, 2014 · 11 revisions

FxA Inter-Service Authentication and Delegation

This document describes the method by which Mozilla web services (called "RP"s, Relying Parties) can allow their users to "Sign In With Your Firefox Account". The RP server will receive proof that the user controls the given FxA, as well as credentials that grant it certain access to data on other servers on behalf of that user.

This uses an OAuth2 flow and a new "fxa-oauth-server" to issue and validate tokens. RPs can use these tokens to convince other servers (known as "Delegated Services") to accept their requests.

Basic Flow

The RP web page redirects the browser to a special login page on the FxA Content Server. The user then enters their email address and FxA password on this page, which verifies them and allocates a secret code, then redirects the browser back to the RP page. The code is then used by the RP backend server to verify the user's identity and obtain the OAuth token it will use for subsequent requests.

Detailed Flow

Before the user even turns on the computer, several things must happen behind the scenes. Each RP which wants to use FxA logins must be registered with the fxa-oauth-server. They provide a callback_uri, and receive a client_id and a shared client_secret.

The real login process starts when the user directs her browser at an RP like the Mozilla Marketplace. The "RP Backend Server" delivers a web page which we call the "RP Frontend Page". This includes a "Sign In With Your Firefox Account" link.

When this link is clicked (1.1), the page redirects the browser (1.2) to a page (1.3) on the fxa-content-server, and includes the following query arguments (which might be included in the page ahead of time, generated by JS with some XHR calls at the moment the login button is clicked, or generated by the RP server via an intermediate redirect):

  • client_id: which pre-registered RP is asking for tokens
  • redirect_uri: an RP URL to which the browser will be returned after login
  • state: a nonce used to bind the outbound redirect with the eventual return
  • scope (optional): space-separated list of desired authorities. (remember that spaces are URL-encoded as "+")

For example, the browser might be redirected to: https://login.accounts.firefox.org/login.html?client_id=3a903bd6a81a2cdc&redirect_uri=https%3A%2F%2Fmarketplace.mozilla.com%2Flogin2.html&state=64c4e4dcfedd9c2d25e8fe8ce017aa31

The fxa-content-server then serves a login.html page (1.4) which describes the name of the RP asking for access (derived from the client_id), the sort of delegated access they're asking for (scope), and asks the user for their email address and password. The content-server page uses email+password in XHR requests (2.1) to the fxa-auth-server's /account/login API endpoint to obtain a session token (2.2), generates a public key, and uses both token and key to obtain a signed certificate (2.4). It uses the cert to create a signed assertion (with audience pointing at the fxa-oauth-server), then forgets the keys and session token (and maybe also destroys the session?).

The page needs to ask fxa-oauth-server for information about the RP, by submitting the client_id and the set of requested scopes to some API. This API should return an RP name, the pre-configured redirect_uri, an optional logo or image of some sort, and maybe a (localized?) string describing the service. For each requested scope, it should return a localized string describing that scope. Finally, it should also return a list of scopes that are whitelisted: if the RP only asks for whitelisted scopes, the user permission check should be skipped.

The page should then present the RP information and scope descriptions to the user, and ask for permission. For extra credit, the user should be able to opt-out of individual permissions, removing them from the set of requested scopes.

The page then uses an API on the new fxa-oauth-server (2.5) to exchange the assertion (and remaining scopes) for a "code" (which is bound to the client_id and scope) (2.8) and verifies that the requested redirect_uri matches the origin registered for that client. Finally, the page redirects the browser back to the RP's redirect_uri, along with several query arguments:

  • state: matches the original redirect argument, to bind them together
  • code: allocated by fxa-oauth-server

(To convert the assertion into a code, the fxa-oauth-server submits the assertion to a browserid verifier that knows the fxa-auth-server's public key (2.6), which verifies the signatures and returns the uid embedded in the certificate (2.7). The fxa-oauth-server then allocates a code, and associates it in an internal table with the account uid and the client_id of the RP. The code is returned to the browser, and the table entry will be used later.)

The RP backend server sees the argument-augmented redirect_uri request (3.2), checks that state matches a flow that it remembers, and extracts code. It them makes an HTTPS POST (3.3) to a different fxa-oauth-server endpoint (meant for RP backend servers, rather than web browsers), which includes the following arguments:

  • client_id: same as before
  • client_secret: the shared secret, to convince fxa-oauth-server that this is the named+registered RP and not a stranger or some other registered RP (1).
  • code: extracted from the return redirect URL

The fxa-oauth-server looks up the client_id, verifies client_secret, then looks up code to make sure it is bound to the given client_id. If everything is ok, it retrieves the scope that was previously requested and removes code from the table. It then allocates a new OAuth2 "access_token", binding it to the client_id and scope. The POST response (3.4) is a JSON blob with these properties:

  • access_token
  • scopes: list of scope identifiers
  • token_type: the literal string "bearer"

If client_secret or code are wrong, or the code is not bound to the right client_id, the fxa-oauth-server will return an error.

The RP receives the POST response. If fxa-oauth-server signalled and error, the client request was invalid, and the RP returns an error page that invites the user to try to log in again. If it indicates success, the RP stores at least access_token in a database, and typically links it to a new RP session token, which is then returned to the user's browser as a cookie. RP pages can then deliver the RP-session-token cookie (plus suitable CSRF defenses) to validate subsequent RP requests.

How RPs Exercise Token-Based Powers

When the RP Backend Server wants to use some of the power it's been granted by the user, it makes an HTTPS-protected request to the corresponding API endpoint (4.1) and includes the access_token in the request. RFC 6750 specifies how the token is included: either in a header, a query argument, or a form field (depending upon the API) (2). The Delegated Service which hosts this API endpoint submits the token to the fxa-oauth-server's "validate-token" API, which either returns a tuple of (account-id, scopes), or an error if the token is invalid. The delegated service then has enough information to decide whether to honor the RP's request or not.

The validate-token API may also return the account's recovery email address. (TBD) The API should neither generate nor validate tokens unless the account's recovery address has been validated.

Security Analysis

OAuth2 is (relatively) standardized and used in large sites like Google and Github, so it's been studied a lot (which is the biggest reason for using it over a home-grown solution). This section merely describes the few items that jumped to mind during our early analysis.

Why client_id is included in the first redirect (1.2)

The IdP has two jobs: to make sure the browser is being driven by the right human, and to ask that human whether it's ok to give the RP power over some aspect of their account.

To ask that question properly, the IdP must somehow describe the (to the human). client_id lets the IdP present an RP/application name and logo (both pre-registered by the RP) during that question. In this respect, client_id serves the same purpose as the Persona "rp_name" string and "rp_logo" image, which are displayed in the Persona login box.

The IdP remembers client_id with the tokens it allocates, making it useful in a subsequent dashboard which displays all outstanding tokens, to tell the user which token is for which RP.

In OAuth2, client_id serves two other purposes. First, it gives the IdP a measure of control over RP applications: at any time, the IdP can simultaneously revoke all tokens generated for the RP (and reject new ones). The threat of this revocation may discourage abusive behavior by the RP.

Second, it enables whitelisting of specific clients. In particular, Mozilla services could automatically be granted certain authority over the account: e.g. Marketplace could be allowed to have profile/read access without explicit user consent, so it can display a human name on its pages. The fxa-oauth-server needs to know which client is asking for permission to decide whether the request should be whitelisted or not.

Why "state" is included in the first redirect (1.2)

This binds the RP's outbound redirect with the response redirect that delivers a code. It prevents a session-fixation attack.

Without it, an attacker could begin the flow, signing into their own IdP account, then stop the process (at step 3.2) before submitting the resulting code to the RP. This code, when eventually delivered to the RP, will empower the session that submits it to control the attacker's account at that RP, as well as access whatever resources the OAuth2 token provides.

Then the attacker presents the code-bearing return-redirect URL to an unsuspecting victim, in a context where they expect to sign into the same RP. When the victim follows the link, their session will be quietly signed into the attacker's RP account. Any information they reveal to the RP at that point may be retrievable later (e.g. sent email) or otherwise useable by the attacker (e.g. credit cards stored by a merchant site for "one-click" purchases).

Adding and checking "state" binds two things together: the user's original attempt to log into the RP, and the subsequent submission of "code" to the RP (which implements the login), which prevents this attack.

This kind of attack was originally discovered in OAuth1.0, however in a different sort of flow.

Why "redirect_uri" must be pre-registered

If a malicious RP ("Evil-RP") is allowed complete control over the redirect_uri (submitted as a query argument in 1.2, used by the return redirect in 3.1), then Evil-RP can request a login with a client_id of its choosing (a benign-sounding app "good-RP", or one which the user has probably already authorized), but attach a redirect_uri pointing to Evil-RP, allowing it to learn a code meant for the other app. It then pretends to be the user while talking to good-RP, skipping the 1.2 - 3.1 login process entirely, and submitting the other code to message 3.2 . Good-RP will then validate this code with the oauth server, it will check out, and Good-RP will grant access (over the user's account information at Good-RP) to the attacker (i.e. to the session which submitted 3.2).

By limiting redirect_uri to a pre-configured value, codes will only be revealed to the right RP. The specific binding is between the RP name displayed to the human during the permission-grant page (keyed by client_id) and the RP server which learns a code that will be valid when submitted by that human-selected RP.

Why client_secret is included in the fxa-oauth-server "verify" message (3.3)

In the best case, the code submitted to the fxa-oauth-server (3.3) was originally generated by fxa-oauth-server on behalf of the right client_id, passed correctly through the user's browser to the RP-provided redirect_uri, and delivered by the correct RP for verification. Since code are unguessable, only fxa-oauth-server -generated codes can possibly be accepted.

??? WIP

However an attacker running one RP ("RP1") might submit their RP1-code to RP2's redirect_uri, instead of

received by the right RP (3.2) was generated by the fxa-oauth-server and passed correctly through the user's browser to the RP-provided redirect_uri. But code might be from an attacker, and this attacker might be a different RP ("RP2"), who has received a code from the same user (meant for RP2).

makes the user-permission question real

Or the code might be from the user themselves, trying to obtain some power (via an oauth token) that they aren't supposed to be able to obtain themselves, something which was supposed to be reserved for the RP's backend server. I don't think this makes sense in the general case, but there may be specific environments (no-backend all-web applications, maybe locked-down smartphone OSes).

Why Aren't fxa-auth-server's BrowserID Assertions Enough?

(still WIP)

The protocols defined in https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol enables basic login: specifically, a client application (in conjunction with a human who knows the account password) can use HTTPS APIs and some brief cryptographic operations to talk to the fxa-auth-server to obtain a signed BrowserID certificate. This certificate can be used to create BrowserID assertions, and these assertions can be used to convince specific RPs (named in the "Audience" of each assertion) that the bearer (i.e. the client app) rightfully has control over the numbered FxA account named in the certificate.

Delegation

But.. Privacy!

Persona/BrowserID offers several privacy and useability improvements over traditional third-party login systems. First, the use of public-key signatures allows RPs to verify certificates from IdPs without first establishing a shared HMAC secret (i.e. RPs do not have to register with the IdP). Second, the use of two separate public-key signatures allows RPs to verify assertions without revealing (to the IdP) which user is signing in.

However the close relationship between the FxA IdP (fxa-auth-server) and the Mozilla services using these logins (RPs) negates these benefits.

(the

Since BrowserID assertions

we need more than this

third-party convincing

delegated authority

web flow

assertion-length: Persona uses internal hidden iframes, to improve privacy (a redirect flow would reveal referrer and redirect URIs to the IdP). Iframes require postMessage. postMessage enables long messages, enabling assertions. But Iframes are difficult for compatibility (JSChannel has all sorts of weird fallbacks for older browsers), whereas redirects are easy. IdP/RP privacy is moot, removing that constraint, making redirects easier to implement compatibly, which imposes length restriction on the message, which prohibits passing assertions through a redirect, requiring "code". (that would still allow using "code" to fetch an assertion)

Missing Features of OAuth2

The need to pre-register RPs makes it unsuitable for a general-purpose login system for the Web. Registration must necessarily be scarce (otherwise the abuse-punishing properties of client_id are lost: abusers will just register a new RP for each user), which means RPs must negotiate with IdPs to be included, which will limit the number of (IdP,RP) pairs that will work, hurting adoption. In the worst case, RPs may exhibit rent-seeking behavior, demanding payment for the "privilege" of allowing their accounts to be used for login on that RP.

In the other direction, the need to hard-wire the login page to which the browser is initially redirected (1.2) limits the number of IdPs that any given RP may be willing to support, leading to the "NASCAR Effect" and making it difficult for new IdPs to compete against larger established players. There are proposals (WebFinger/etc) to implement a discovery step that resemble's Persona's discovery step, but none have much traction.

The need to submit client_id during the login process immediately tells the IdP which RP each user is accessing, in opposition to one of Persona's explicit privacy goals. Even if client_id were removed, the same information leak would occur when the RP verifies their code through an online protocol with the OAuth server. In Persona, this was addressed with two-signature certificates and offline verification.

Footnotes

  • (1): it might be better to use the shared secret as an HMAC key, instead of as a bearer token, but doing so is non-trivial and requires the fxa-oauth-server to store all of these secrets verbatim

  • (2): again, RP backend servers want to use a better proof-of-knowledge technique when exercising the access_token in requests to delegated services.

Clone this wiki locally