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)

I'm not yet sure. I think state and the pre-registered redirect_uri are sufficient to keep codes secret (only revealed to the right parties). Codes are unguessable, so the only way to get a code that will validate is for it to come from the fxa-oauth-server in the first place. Requiring that the code be submitted along with the RP client_id should be sufficient to assure the RP that the code was meant for them and not for someone other RP. Including client_secret may provide additional "belt-and-suspenders" security margin.

One thing it does do is to distinguish between the legitimate user and the RP server that they've authorized. It isn't clear when it would be useful to make this distinction: the user nominally has more power over their own account than the RP does. But there may be some environments where this makes sense (no-backend all-web applications? native apps on locked-down smartphone OSes?).

The final access_tokens that come back in 3.4 are labelled with a particular client, but not actually bound to them: anybody who learns the token can use it, even other RPs. So it is important to make sure they're only granted to the right RP (which can then choose to share it if they see fit).

The most likely attack would be 3.3, where attacker EvilRP (who somehow knows a code that was allocated to GoodRP) submits it along with GoodRP's (public) client_id. If this is accepted, EvilRP will be given oauth tokens that were meant for GoodRP. EvilRP might get this from a collusion with the legitimate user (who has control over these resources anyways), or through some bug that reveals GoodRP's code.

Fundamentally, it's necessary to make the user-permission question "real", by enforcing the relationship between the RP named in the question, and the RP that eventually gets the power. There are two things that make the "right" RP special: the client_secret and the pre-configured redirect_uri.

There may be flows in which redirect_uri is not used (native apps?), and thus does not protect the code against leaks that would allow EvilRP to learn it.

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