1 oauth design
Ryan Kelly редактировал(а) эту страницу 2018-11-13 09:55:51 +11:00

FxA Inter-Service Authentication and Delegation

This document describes the method by which Mozilla web services (called "RP"s, Relying Parties, and known as "Consumers" in the OAuth world) can allow their users to "Sign In With Your Firefox Account". The RP server will receive proof that the user (the "Resource Owner") 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", or "Service Providers" in OAuth) 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 redirect_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.

images/RP-login.png

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

images/grant.png

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.)

images/get-token.png

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

images/use-token.png

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.

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.