ampproject / wg-amp4email

Responsible for the AMP4Email project. Facilitator: @nainar
Creative Commons Attribution 4.0 International
56 stars 14 forks source link

Discussion: Authentication in AMP emails #19

Open fstanis opened 4 years ago

fstanis commented 4 years ago

Background

Authentication using ID tokens

On a high level, when a user is authenticated, their client attaches a unique identifier (referred to as ID token in further text) to HTTP requests it sends. This ID token is resolved on the server side and maps to the user's identity.

The preferred way to attach ID tokens to HTTP requests is using HTTP cookies. Email clients, however, block HTTP cookies for privacy and security reasons. This means that emails can neither read existing cookies from the sender's website (and determine if the user is already logged in) nor set new cookies.

Using ID tokens in the URL

Because cookies can't be used, emails attach ID tokens to the URL itself. As the ID token is part of the email content, it's only visible inside the recipient's inbox. In other words, the ID token is tied to the user authenticating with their email client, making this a viable method to assert the user's identity.

The most common existing use of this method are password reset emails. For example, a website may send an email that contains a hyperlink to https://website.example/accounts/resetpassword?user=17436e4c45c62e88cfe78fea37ce002f where 17436e4c45c62e88cfe78fea37ce002f is an ID token that uniquely identifies the user attempting to reset their password. In addition to being uniquely associated with the user, this token is limited in scope (it can only be used for one purpose, resetting the password) and is time-limited.

Authentication in AMP emails

The same method of authentication (using ID tokens in URLs) can already be used in AMP for Email. For example, an <amp-list> may similarly use an ID token to identify the user:

<amp-list src="https://website.example/recommendations?user=b14a9d005403979154389c851e1a9fb2">
  <template type="amp-mustache">
    ...
  </template>
</amp-list>

In this case, the server endpoint can use b14a9d005403979154389c851e1a9fb2 to uniquely identify the user and return data personalized to them.

Session hijacking attack

Assuming an attacker somehow successfully obtains the email source code outlined above, they are able to manually make an HTTP request to the endpoint used by <amp-list> and receive the list of recommendations for another user.

$ curl -H "Accept: application/json" https://website.example/recommendations?user=b1...
{ "items": [ "Alice's recommendations ..." ] }

This document proposes a method to mitigate this type of attack: email client assertion tokens. Separate measures can and should be taken to minimize the possibility of this attack happening in the first place, but this is beyond the scope of this document and proposal.

Email client assertion tokens

Overview

An email client assertion token allows an HTTP server to differentiate between HTTP requests made from within an AMP email rendered inside an email client from requests made through other means (web browser, curl or any other HTTP client).

Specifically, an email client assertion token is a type of digital signature that can only be generated by the entity hosting the recipient's inbox, determined based on the recipient email's domain (e.g. email.example for user@email.example).

High level token generation and verification flow

The types of HTTP requests eligible to contain an email client assertion token are:

A prerequisite for implementing email client assertion tokens is for the email to already be using a proxy server for these requests, as the tokens can only be generated on the email client's server-side.

When a request is made through one of these two methods, the following flow happens:

  1. The AMP runtime forwards the HTTP request to the email client's proxy server.
  2. The email client's server generates a token that's digitally signed with the email client's private key. This token also contains the sender's email address.
  3. The email client sends the HTTP request to the URL of the original request (specified in the src or action-xhr attribute) and adds the assertion token to the AMP-Email-Assertion-Token HTTP header.
  4. The email sender's server receives the assertion token and verifies the following:
    1. That the sender email contained inside matches the email they used.
    2. That the signature is valid for the owner of the recipient email's domain name.
  5. An invalid token may mean the request was made by a malicious attacker. The email sender chooses how to treat this type of request (e.g. dismissing it entirely or returning only public data when applicable).

Detailed token specification

Email client assertion tokens are JWT-based tokens signed with the email client's private key and generated in the proxy server used for amp-list and amp-form requests. This token contains:

  1. Sender email in the audience field
  2. Email client identifier (home page, e.g. https://email.example) in the issuer field
  3. Time at which the JWT was issued in the issued at field.

Example token payload:

{
  "iss": "https://email.example",
  "aud": "sender@sender.example",
  "iat": 1516239022
}

The public key used to verify the token is hosted on the HTTP server the email client controls in the .well-known folder per RFC 5785, specifically https://&lt;key_domain>/.well-known/ampforemail-keys

The key_domain represents a domain controlled by the email client. The process to determine key_domain is as follows:

  1. Read the domain from the recipient email, e.g. user@email.example indicates the domain is email.example. This domain is referred to as recipient_domain.
  2. Read the TXT records from the recipient_domain. If there's a record in the format of ampforemail-keys-domain=<domain>, then <domain> is the key_domain. For example, if email.example has a TXT record that equals ampforemail-keys-domain=emailclient.example, then emailclient.example is the key_domain. If the key_domain is found, skip the remaining steps.
  3. If no matching TXT record is found, read the MX records of recipient_domain in order of priority.
  4. For each domain in the MX record, read the TXT records. If there's a record in the format of ampforemail-keys-domain=<domain>, then <domain> is the key_domain.
  5. If no matching TXT record is found in any of the domains from the MX records, treat the JWT payload as invalid.

If a valid key_domain is found, then a GET request is made to fetch the file at https://&lt;key_domain>/.well-known/ampforemail-keys. If the HTTP request fails, treat the JWT payload as invalid.

The response from this HTTP request can be cached locally. It's expected that the HTTP server returns an Expires header to indicate how long the public key can be cached for, but other standard mechanisms (such as Last-Modified and ETag) can also be used.

ampforemail-keys contains a PEM encoded public key in the ASN.1 as defined in the X.509 standard. This key is used to verify the signature of the JWT data.

The presence of a valid, signed JWT token in the request guarantees to the sender that the HTTP request has been made by the entity that controls the domain of the recipient's email.

Fallback for non-supporting email clients

Initially, email clients are expected to opt-in by adding data-amp-assertion-token-opt-in to the <html> element. This signals to the AMP runtime that the email client supports this feature.

The JWT token is only attached if requested in the markup, via the include-assertion attribute, e.g. <amp-list src="..." include-assertion> or <form method="post" action-xhr="..." include-assertion>. If the email client doesn't support assertion tokens (data-amp-assertion-token-opt-in not present in <html>), then the AMP runtime never makes XHRs for elements that have include-assertion and immediately displays the fallback for amp-list.

If it supports this feature, an email client may choose to always include the assertion token (even when include-assertion isn't present). The purpose of include-assertion is to indicate a server endpoint requires assertion tokens, rather than to limit sending them.

Caveats and limitations

avigoldman commented 4 years ago

Would it be possible to include the recipient email address in the JWT payload? That way the server endpoint can identify which user the request was made on behalf of.

Example:

{
  "iss": "https://email.example",
  "aud": "sender@sender.example",
  "rcpt": "user@email.example",
  "iat": 1516239022
}
fstanis commented 4 years ago

Thanks for raising that! We discussed something to that effect on a few occasions - the issues previously raised were:

(note that not all of these are supported by all email clients supporting AMP, e.g. forwarding AMP emails isn't possible, but we don't want to inadvertently define this feature in such a way it limits other email features)


Finally, even if these are addressed, we'd prefer to encourage senders to instead generate limited-use ID tokens (in both scope and time), as this is a good overall security practice.

avigoldman commented 4 years ago

That makes sense, glad to hear it was discussed! The canonical email problem is a concern. It could potentially be solved by adding a header (something like X-AMP-RCPT) to the email?

I still think it is worthwhile to do for a few reasons:

  1. It decreases the engineering effort to implement an AMP email
  2. It eliminates the possibility of the limited-use ID tokens expiring before the AMP email does.
  3. It creates a standard for how authentication in AMP should be handled
  4. It makes it easier for email platforms like SparkPost and SendGrid to support AMP tracking for their customers.

Obviously, these are very rough thoughts and it's always good to lean towards more secure solutions 😅

fstanis commented 4 years ago

Thanks, these are some thoughtful points. At this point, we're in territory that wasn't discussed before at a WG meeting, so what follows is just my personal opinion. :slightly_smiling_face:

Adding a new header is probably not a good idea. It wouldn't (by default) be included in the DKIM signature, opening it up to potential tampering. This would mean ESPs would need to adjust and always include it and also potentially email clients requiring this header to be included, so a lot of work on the ecosystem as a whole.

When it comes to engineering effort... this is a double-edged sword. My view is that we don't want to decrease the effort to do something potentially dangerous (using an email as opposed to a limited use token). Certainly, we don't want to limit it per se, but authentication is a serious component and I'd prefer that whomever uses it in an email has made the conscious decision on what they want to use as an identifier.

It makes it easier for email platforms like SparkPost and SendGrid to support AMP tracking for their customers.

I'm not sure I understand this point: these platforms already support modifying the URL for things like links and images, so I'd say they'd have no trouble also inserting a limited use access token to e.g. amp-list before sending.