Authenticating requests in AMP for Email

Dynamic personalized email content often requires authenticating the user. However, to protect user data all HTTP requests made from inside AMP emails within Gmail are proxied and stripped of cookies.

To authenticate requests made from AMP emails, you must use access tokens, and optionally add an extra level of security through the use of proxy assertion tokens.

Access tokens

You can use access tokens to authenticate the user. Access tokens are supplied and checked by the email sender. The sender uses the tokens to ensure that only those with access to the AMP email can make the requests contained within that email. Access tokens must be cryptographically secure and time- and scope-limited. They are included within the URL of the request.

This example demonstrates using <amp-list> to display authenticated data:

<amp-list src="https://example.com/endpoint?token=REPLACE_WITH_YOUR_ACCESS_TOKEN"
  height="300">
  <template type="amp-mustache">
    ...
  </template>
</amp-list>

Similarly when using <amp-form>, place your access token in the action-xhr URL. The access token must be placed in the URL, not a hidden field, as only the URL is checked when using proxy assertion tokens.

<form action-xhr="https://example.com/endpoint?token=REPLACE_WITH_YOUR_ACCESS_TOKEN" method="post">
  <input type="text" name="data">
  <input type="submit" value="Send">
</form>

Example

The following example considers a hypothetical note-taking service that lets logged-in users to add notes to their account and view them later. The service wants to send an email to a user, jane@example.com, that includes a list of notes they previously took. The list of the current user's notes is available at the endpoint https://example.com/personal-notes in JSON format.

Before sending the email, the service generates a cryptographically secure limited-use access token for jane@example.com: A3a4roX9x. The access token is included in the field name exampletoken inside the URL query:

<amp-list src="https://example.com/personal-notes?exampletoken=A3a4roX9x" height="300">
  <template type="amp-mustache">
    <p>{{note}}</p>
  </template>
</amp-list>

The endpoint https://example.com/personal-notes is responsible for validating the exampletoken parameter and finding the user associated with the token.

For more information, see Limited use access tokens.

Proxy assertion tokens

All requests made to endpoints used in <amp-list> and <amp-form> are routed through Gmail's proxy. Adding an access token lets an email sender ensure that only those with access to the email they send can perform actions within that email.

An extra level of security can be achieved by using proxy assertion tokens. These JWT-based tokens are added by Gmail's proxy to all XMLHttpRequests (XHRs) and, once verified by you, ensure that:

  1. The request came from the Gmail proxy server.
  2. The current Gmail user (verified using Google cookies) received a valid AMP email from the email address within the Audience field of the proxy assertion token. Gmail must have received this email within the last 31 days.
  3. A user is requesting a URL within that valid AMP email.

This prevents abuse in the case of leaked email contents, where the action tokens would be compromised and could be used by anyone.

To use proxy assertion tokens, follow these steps:

  1. When registering to send AMP emails to Gmail users, please include in your request that your emails require proxy assertion tokens.
  2. On your endpoint, extract the Amp4Email-Proxy-Assertion header from the inbound HTTP requests. If this header isn't present, respond to the request with an HTTP response code 401 (Unauthorized).
  3. Parse the proxy assertion token inside the header using a JWT parser. See Verifying tokens for an example.
  4. Verify the JWT signature authenticity using the signing keys provided by Gmail. Gmail's certificate in production is hosted on https://www.googleapis.com/service_accounts/v1/metadata/x509/gmail@system.gserviceaccount.com.
  5. Verify the issuer and audience fields. Ensure that:
    1. issuer field is set to gmail@system.gserviceaccount.com.
    2. audience field is set to https://www.googleapis.com/gmail/amp/<sender email>. For example, if the email is sent from sender@example.com, the audience is https://www.googleapis.com/gmail/amp/sender@example.com. If the above fields are not correct, respond to the request with an HTTP response code 401 (Unauthorized).

Playground

Proxy assertion tokens can be optionally sent from the AMP for Email Playground by selecting the appropriate radio button. These are meant for testing purposes only.

These proxy assertion tokens are different compared to tokens used by Gmail in production in the following ways:

  • Certificates used by playground are hosted on https://www.googleapis.com/service_accounts/v1/metadata/x509/dynamic-mail-hourly@system.gserviceaccount.com.
  • The issuer is set to dynamic-mail-hourly@system.gserviceaccount.com.
  • The email sender is amp@gmail.dev and therefore the audience is https://www.googleapis.com/gmail/amp/amp@gmail.dev.
  • The token is long-lived (1 year).
  • The target server is required to set the Access-Control-Allow-Headers response header to include Amp4Email-Proxy-Assertion for your browser to permit the response.

Verifying Tokens

The following examples show how to parse and verify a proxy assertion token. The open source Google API Client Libraries provide methods that assist with verification:

Java

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;

import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
import com.google.api.client.googleapis.auth.oauth2.GooglePublicKeysManager;
import com.google.api.client.http.apache.ApacheHttpTransport;
import com.google.api.client.json.jackson2.JacksonFactory;

public class TokenVerifier {
    // Proxy assertion tokens in production specify this issuer.
    private static final String GMAIL_ISSUER_PRODUCTION =
          "gmail@system.gserviceaccount.com";
    // Proxy assertion tokens generated by Playground specify this issuer.
    private static final String GMAIL_ISSUER_PLAYGROUND =
          "dynamic-mail-hourly@system.gserviceaccount.com";

    // When testing with playground, change to GMAIL_ISSUER_PLAYGROUND.
    private static final String GMAIL_ISSUER = GMAIL_ISSUER_PRODUCTION;

    // URL to fetch the certificate from.
    private static final String PUBLIC_CERT_URL =
          "https://www.googleapis.com/service_accounts/v1/metadata/x509/"
          + GMAIL_ISSUER;

    // The sender email addresses used by Playground.
    private static final Collection<String> SENDER_EMAILS_PLAYGROUND =
          Arrays.asList("amp@gmail.dev");
    // Your sender email addresses which send the email containing AMPHTML.
    private static final Collection<String> SENDER_EMAILS_PRODUCTION =
          Arrays.asList("sender@example.com", "another@example.com");

    // When testing with playground, change to SENDER_EMAILS_PLAYGROUND.
    private static final Collection<String> SENDER_EMAILS =
          SENDER_EMAILS_PRODUCTION;

    // The token audience always starts with this URL, followed by the
    // sender email.
    private static final String AUDIENCE_PREFIX =
          "https://www.googleapis.com/gmail/amp/";

    // Allowed audiences for proxy assertion token (concatenated emails
    // with AUDIENCE_PREFIX).
    private static final Collection<String> AUDIENCES =
            new ArrayList<String>() {{
                for (String email : SENDER_EMAILS) {
                    add(AUDIENCE_PREFIX + email);
                }
              }};

    public static void main(String[] args) throws
            GeneralSecurityException, IOException {
        // Get this value from the request's Amp4Email-Proxy-Assertion
        // HTTP header.
        String proxyAssertionToken = "AbCdEf123456";

        GooglePublicKeysManager keyManager =
                new GooglePublicKeysManager.Builder(
                        new ApacheHttpTransport(), new JacksonFactory())
                    .setPublicCertsEncodedUrl(PUBLIC_CERT_URL)
                    .build();

        GoogleIdTokenVerifier verifier =
                new GoogleIdTokenVerifier.Builder(keyManager)
                    .setIssuer(GMAIL_ISSUER)
                    .setAudience(AUDIENCES)
                    .build();

        GoogleIdToken idToken = verifier.verify(proxyAssertionToken);
        if (idToken == null) {
            System.out.println("Invalid token");
            System.exit(-1);
        }

        // Token originates from Google and is targeted to a
        // specific client.
        System.out.println("The token is valid");

        System.out.println("Token details:");
        System.out.println(idToken.getPayload().toPrettyString());
    }
}

Python

#!/usr/bin/python3

import sys
from oauth2client import client

# Proxy assertion tokens in production specify this issuer.
GMAIL_ISSUER_PRODUCTION = 'gmail@system.gserviceaccount.com'
# Proxy assertion tokens generated by Playground specify this issuer.
GMAIL_ISSUER_PLAYGROUND = 'dynamic-mail-hourly@system.gserviceaccount.com'

# When testing with playground, change to GMAIL_ISSUER_PLAYGROUND.
GMAIL_ISSUER = GMAIL_ISSUER_PRODUCTION

# URL to fetch the certificate from
PUBLIC_CERT_URL =
  'https://www.googleapis.com/service_accounts/v1/metadata/x509/'
  + GMAIL_ISSUER

# The sender email addresses used by Playground.
SENDER_EMAILS_PLAYGROUND = { 'amp@gmail.dev' }

# Your sender email addresses which send the email containing AMPHTML.
SENDER_EMAILS_PRODUCTION = { 'sender@example.com', 'another@example.com' }

# When testing with playground, change to SENDER_EMAILS_PLAYGROUND.
SENDER_EMAILS = SENDER_EMAILS_PRODUCTION

# The token audience always starts with this URL, followed by the sender
# email.
AUDIENCE_PREFIX = 'https://www.googleapis.com/gmail/amp/'

# Allowed audiences for proxy assertion token (concatenated emails
# with AUDIENCE_PREFIX).
AUDIENCES = { AUDIENCE_PREFIX + sender for sender in SENDER_EMAILS }

try:
  # Get this value from the request's Amp4Email-Proxy-Assertion
  # HTTP header.
  proxy_assertion_token = 'AbCdEf123456'

  # Verify valid token, signed by google.com, intended for a third party.
  token = client.verify_id_token(proxy_assertion_token, None,
      cert_uri = PUBLIC_CERT_URL)
  print('Token details: %s' % token)

  if token['iss'] != GMAIL_ISSUER:
    sys.exit('Invalid issuer')
  if token['aud'] not in AUDIENCES:
    sys.exit('Invalid audience')
except:
  sys.exit('Invalid token')

# Token originates from Google and is targeted to a specific client.
print('The token is valid')