AMP

AMP における CORS

多くの AMP コンポーネントと拡張機能は、クロスオリジンリソースシェアリング(CORS)リクエストを使用することで、リモートエンドポイントを活用しています。このドキュメントでは、AMP で CORS を使用する際の主な側面について説明します。CORS そのものについては、W3 CORS Spec を参照してください。

自分のオリジンに CORS を使用する必要があるのはなぜですか?

自分のオリジンへのリクエストになぜ CORS を使用する必要があるのかがわからない方もいるでしょう。では、その理由を詳しく探ってみましょう。

動的データをフェッチする AMP コンポーネント(amp-form、amp-list など)は、データを取得するためにリモートエンドポイントに CORS リクエストを送信します。AMP ページにそのようなコンポーネントが含まれる場合は、それらのリクエストが失敗しないように、CORS を処理する必要があります。

これを、例を使って説明します。

製品の一覧を価格付きで示す AMP ページがあるとします。ページの価格を更新するには、ユーザーはボタンをクリックし、それによって JSON エンドポイントから最新の価格が取得されます(amp-list コンポーネントを使用)。この JSON はあなたのドメイン上にあります。

ページが自分のドメインにあり、JSON も自分のドメインにあるなら、問題はありませんよね!

そうですね。でも、ユーザーは、どのようにして AMP ページにたどり着いたのでしょうか。アクセスしているのはキャッシュされたページですか?ユーザーが AMP ページに直接アクセスせずに、別のプラットフォームからページにたどり着いた可能性が非常に高いといえます。たとえば、Google 検索では、AMP ページを素早くレンダリングできるように、Google AMP キャッシュを使用しています。これらのページはキャッシュされたページで、Google AMP キャッシュから配信されます。これは、異なるドメインです。ユーザーがボタンをクリックして価格と更新しようとすると、キャッシュされた AMP ページは、価格を取得するために、オリジンドメイン(あなたのドメイン)にリクエストを送信するわけですが、オリジン間の不一致(キャッシュ -> オリジンドメイン)が生じます。このようなクロスオリジンリクエストを許可するには、CORS を処理する必要があります。処理しなければ、リクエストは失敗してしまいます。

ではどうすればいいですか?

  1. 動的データをフェッチする AMP ページの場合は、自分のドメインでだけでなく、キャッシュされたページも必ずテストしてください。(以下の「AMP での CORS のテスト」セクションを参照してください。)
  2. CORS リクエストとレスポンスの処理について、このドキュメントの指示に従ってください。

CORS リクエストを使用するほとんどの AMP コンポーネントは、クレデンシャルモードを自動的に設定するか、作成者が任意に有効化することができます。たとえば、amp-list コンポーネントは CORS JSON エンドポイントから動的コンテンツをフェッチし、作成者が credentials 属性を通じてクレデンシャルモードを設定することを許可します。

例: cookie を介して amp-list にパーソナライズコンテンツを含める

<amp-list
credentials="include"
src="<%host%>/json/product.json?clientId=CLIENT_ID(myCookieId)"

>   <template type="amp-mustache">

    Your personal offer: ${{price}}
  </template>
</amp-list>

クレデンシャルモードを指定すると、オリジンは、CORS リクエストに cookie を含めてレスポンスに設定することができます(サードパーティ cookie の制限が適用されます)。

AMP でのクレデンシャル CORS リクエストには、ブラウザに指定されたサードパーティ cookie の制限が適用されます。これらの制限はブラウザやプラットフォームによって異なりますが、一部のブラウザでは、ユーザーが過去にファーストパーティ(トップ)ウィンドウでオリジンを訪問したことがなければ、オリジンが cookie を設定できなくなっています。言い換えると、ユーザーがオリジンのウェブサイトに直接訪問したことがある場合のみ、cookie が設定されるということになります。このため、CORS を介してアクセスされるサービスにおいて、デフォルトで cookie が設定されると考えてはいけません。

AMP における CORS セキュリティ

AMP ページのリクエストとレスポンスの有効性と安全性を確保するには、以下を行う必要があります。

  1. リクエストを検証する
  2. 適切なレスポンスヘッダーを送信する

バックエンドで Node を使用している場合は、AMP CORS ミドルウェアを使用できます。これは、AMP Toolbox の一部です。

CORS リクエストの検証

エンドポイントが CORS リクエストを受信したら、次の項目を行います。

  1. CORS Origin ヘッダーが許可されたオリジンであることを検証する(サイト運営者のオリジンと AMP キャッシュ)
  2. Origin ヘッダーがない場合は、リクエストが同一のオリジンから発行されたものであることを確認する(AMP-Same-Origin

1) 特定委の CORS オリジンのリクエストを許可する

CORS エンドポイントは、Origin HTTP ヘッダーを通じてリクエストを発行するオリジンを受け取ります。エンドポイントが許可するリクエストは、(1)サイト運営者自身のオリジンであり、(2)https://cdn.ampproject.org/caches.json に記載される cacheDomain オリジンである必要があります。

たとえば、エンドポイントは、以下から送られるリクエストを許可します。

  • Google AMP Cache subdomain: https://<publisher's domain>.cdn.ampproject.org
    (for example, https://nytimes-com.cdn.ampproject.org)

AMP キャッシュの URL 形式については、以下のリソースを参照してください。

2) same-origin リクエストを許可する

Origin ヘッダーが欠落している same-origin リクエストの場合、AMP は以下のカスタムヘッダーを設定します。

AMP-Same-Origin: true

このカスタムヘッダーは、XHR リクエストが同一のオリジンで発行されたものである場合に AMP ランタイムによって送信されます(キャッシュなしの URL から配信されるドキュメント)。AMP-Same-Origin:true ヘッダーを含むリクエストを許可してください。

CORS レスポンスヘッダーの送信

CORS リクエストを検証した後に生成される HTTP レスポンスには、以下のヘッダーが含まれている必要があります。

Access-Control-Allow-Origin: <origin>

このヘッダーは、origin は CORS Origin リクエストヘッダーで許可されたリクエスト元のオリジン("https://<サイト運営者のサブドメイン>.cdn.ampproject.org" など)を参照するという W3 CORS Spec の要件です。

W3 CORS 仕様書では、レスポンスで * の値を返すことを許可していますが、セキュリティを改善するために、以下のようにする必要があります。

  • Origin ヘッダーが存在する場合は、Origin ヘッダーの値を検証してエコーする。

状態が変化するリクエストの処理

リクエストを処理するに、以下の検証チェックを実行してください。この検証によって、CSRF 攻撃に対する保護を得、信頼できないソースのリクエストを処理しないようにすることができます。

システムの状態を変更する可能性のあるリクエスト(ユーザーがメーリングリストを購読するか購読解除するなど)を処理する前に、以下のことを確認します。

Origin ヘッダーが設定されている場合:

  1. オリジンが以下の値のいずれかに一致しない場合は、中断してエラーレスポンスを返します。
  • <publisher's domain>.cdn.ampproject.org
  • サイト運営者のオリジン(あなたのオリジン)

* は、ワイルドカード一致であり、実際のアスタリスク(*)ではありません。

  1. 含まれている場合は、リクエストを処理します。

Origin ヘッダーが設定されていない場合:

  1. リクエストに AMP-Same-Origin: true ヘッダーが含まれているかどうかを確認します。このヘッダーが含まれていない場合は、中断してエラーレスポンスを返します。
  2. 一致する場合は、リクエストを処理します。

サンプルウォークスルー: CORS リクエストとレスポンスの処理

エンドポイントへの CORS リクエストで考えられるシナリオには 2 つあります。

  1. 同一のオリジンから送信されるリクエスト。
  2. キャッシュされたオリジン(AMP キャッシュ)から送信されるリクエスト。

これらのシナリオを例を使って考察してみましょう。この例では、article-amp.html という AMP ページをホストする example.com サイトを管理しています。この AMP ページには、同じく example.com サイトにホストされた data.json ファイルから動的データを取得する amp-list が含まれます。ここでは、AMP ページから発信される data.json ファイルへのリクエストを処理したいと思います。これらのリクエストは、同じオリジンの AMP ページ(非キャッシュ)または別のオリジンの AMP ページ(キャッシュ)から送信されます。

許可されるオリジン

CORS と AMP についてわかっていることに基づいて(上記の「CORS リクエストの検証」より)、この例では以下のドメインからのリクエストを許可することにします。

  • example.com --- サイト運営者のドメイン
  • example-com.cdn.ampproject.org --- Google AMP キャッシュのサブドメイン

許可されるリクエストに対するレスポンスヘッダー

許可されるオリジンから送られるリクエストについては、レスポンスには以下のヘッダーを含めます。

Access-Control-Allow-Origin: <origin>

以下は、この CORS レスポンスに追加するレスポンスヘッダーです。

Access-Control-Allow-Credentials: true
Content-Type: application/json
Access-Control-Max-Age: <delta-seconds>
Cache-Control: private, no-cache

疑似 CORS ロジック

CORS リクエストとレスポンスを処理するロジックは、以下の疑似コードに簡略化できます。

IF CORS header present
IF origin IN allowed-origins
allow request & send response
ELSE
deny request
ELSE
IF "AMP-Same-Origin: true"
allow request & send response
ELSE
deny request

CORS サンプルコード

以下は、CORS リクエストとレスポンスの処理に使用できるサンプルの JavaScript です。

function assertCors(req, res, opt_validMethods, opt_exposeHeaders) {
var unauthorized = 'Unauthorized Request';
var origin;
var allowedOrigins = [
'https://example.com',
'https://example-com.cdn.ampproject.org',
'https://cdn.ampproject.org',
];
var allowedSourceOrigin = 'https://example.com'; //publisher's origin
// If same origin
if (req.headers['amp-same-origin'] == 'true') {
origin = sourceOrigin;
// If allowed CORS origin & allowed source origin
} else if (
allowedOrigins.indexOf(req.headers.origin) != -1 &&
sourceOrigin == allowedSourceOrigin
) {
origin = req.headers.origin;
} else {
res.statusCode = 403;
res.end(JSON.stringify({message: unauthorized}));
throw unauthorized;
}

res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Allow-Origin', origin);
}

注意: 実際に稼働するコードサンプルについては、amp-cors.js を参照してください。

シナリオ 1: 同一オリジンの AMP ページからリクエストを取得する

以下のシナリオでは、article-amp.html ページが data.json ファイルをリクエストします。オリジンは同一です。

リクエストをよく見ると、以下の内容が見つかります。

Request URL: https://example.com/data.json
Request Method: GET
AMP-Same-Origin: true

このリクエストは同じオリジンから送信されるものであるため、Origin ヘッダーはありませんが、カスタム AMP リクエストヘッダーの AMP-Same-Origin: true が存在します。このリクエストは同じオリジン(https://example.com)から送信されたものであるため、許可することができます。

レスポンスヘッダーは、以下のようになります。

Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://example.com

シナリオ 2: キャッシュされた AMP ページからリクエストを取得する

以下のシナリオでは、Google AMP キャッシュにキャッシュされた article-amp.html ページが data.json ファイルをリクエストしているため、オリジンが異なります。

リクエストをよく見ると、以下の内容が見つかります。

Request URL: https://example.com/data.json
Request Method: GET
Origin: https://example-com.cdn.ampproject.org

このリクエストには Origin ヘッダーが含まれているため、許可されるオリジンから送信されたものであるかを検証します。これは許可されるオリジンから送信されたものであることがわかるため、このリクエストを許可することができます。

レスポンスヘッダーは、以下のようになります。

Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://example-com.cdn.ampproject.org

キャッシュされたフォントの使用

Google AMP キャッシュは、AMP HTML ドキュメント、画像、およびフォントをキャッシュすることで、AMP ページの速度を最適化しています。AMP ページを高速化する一方で、キャッシュされたリソースの安全を維持することも大切です。そのため、AMP キャッシュが、オリジンの Access-Control-Allow-Origin 値を尊重しながら、キャッシュされたリソース(通常、フォント)に対してどのように応答するかを変更する必要があります。

過去の振る舞い(2019 年 10 月以前)

AMP ページが @font-face src 属性の https://example.com/some/font.ttf を読み込む際、AMP キャッシュはフォントファイルをキャッシュして、以下のように Access-Control-Allow-Origin にワイルドカードを使用してリソースを配信していました。

  • URL https://example-com.cdn.ampproject.org/r/s/example.com/some/font.tff
  • Access-Control-Allow-Origin: *

新しい振る舞い(2019 年 10 月以降)

現在の実装は許容範囲内ですが、これによりクロスオリジンサイトのフォントが予期せずに使用される可能性があります。この変更では、AMP キャッシュはオリジンサーバーが応答するのとまったく同じ Access-Control-Allow-Origin 値で応答し始めます。キャッシュされた AMP ドキュメントからフォントを正しく読み込むために、ヘッダーで AMP キャッシュのオリジンを受け入れる必要があります。

以下は実装の例です。

function assertFontCors(req, res, opt_validMethods, opt_exposeHeaders) {
var unauthorized = 'Unauthorized Request';
var allowedOrigins = [
'https://example.com',
'https://example-com.cdn.ampproject.org',
];
// If allowed CORS origin
if (allowedOrigins.indexOf(req.headers.origin) != -1) {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
} else {
res.statusCode = 403;
res.end(JSON.stringify({message: unauthorized}));
throw unauthorized;
}
}

例として、https://example.com/amp.html に /some/font.ttf を読み込む場合、オリジンサーバーは、以下のように、Access-Control-Allow-Origin ヘッダーを使って応答する必要があります。

フォントファイルがあらゆるオリジンからアクセスできるようになっている場合は、Access-Control-Allow-Origin のワイルドカードで応答でき、AMP キャッシュもその値をエコーして、Access-Control-Allow-Origin: * で応答するようになります。すでにこの設定が使用されている場合は、何の変更もいりません。

この変更は 2019 年 10 月半ばに適用する予定です。フォントを独自にホストしているすべての AMP サイト運営者は、この変更による影響があるかどうかを確認してください。

ロールアウトの計画

  • 2019/9/30: リリースには、この変更が適用されるドメインに対するより正確な制御が含まれています。 このビルドは今週中にロールアウトされる予定です。
  • 2019/10/7: テストドメインで手動テストを実行できるようになります。
  • 2019/10/14(テストの結果によって変更の可能性あり): 機能は、一般に向けてロールアウトされます。

関連する課題はこちらでフォローしてください。

AMP での CORS のテスト

AMP ページをテストする際は、AMP ページのキャッシュバージョンもテストに含めるようにしてください。

キャッシュ URL でページを確認する

キャッシュされた AMP ページが正しく表示されて機能することを確認します。

  1. ブラウザで、AMP ページにアクセスするために AMP キャッシュが 使用する URL を開きます。キャッシュ URL の形式は、こちらの AMP By Example のツールで確認できます。

例:

  • URL: https://amp.dev/documentation/guides-and-tutorials/start/create/
  • AMP キャッシュ URL 形式: https://www-ampproject-org.cdn.ampproject.org/c/s/www.ampproject.org/docs/tutorials/create.html
  1. ブラウザの開発ツールを開き、エラーがないこととすべてのリソースが正しく読み込まれることを確認します。

サーバーのレスポンスヘッダーを確認する

サーバーが正しい HTTP レスポンスヘッダーを送信しているかどうかは、curl コマンドを使用して確認することができます。curl コマンドに、リクエスト URL と追加する任意のカスタムヘッダーを指定します。

構文: curl <request-url> -H <custom-header> - I

同一のオリジンから送信されるリクエストのテスト

same-origin リクエストでは、AMP システムによってカスタムの AMP-Same-Origin:true ヘッダーが追加されます。

以下は、 https://ampbyexample.com から examples.json ファイル(同じドメイン上)へのリクエストをテストするための curl コマンドです。

curl 'https://amp.dev/static/samples/json/examples.json' -H 'AMP-Same-Origin: true' -I

コマンドの結果に、正しいレスポンスヘッダーが示されます(注意: 余計な情報は省略されています)。

HTTP/2 200
access-control-allow-headers: Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token
access-control-allow-credentials: true
access-control-allow-origin: https://ampbyexample.com
access-control-allow-methods: POST, GET, OPTIONS

キャッシュされた AMP ページから送信されるリクエストのテスト

同一ドメインからではない (キャッシュ)CORS リクエストでは、リクエストの一部に origin ヘッダーが含まれます。

以下は、Google AMP キャッシュにキャッシュされた AMP ページから examples.json ファイルへのリクエストをテストするための curl コマンドです。

curl 'https://amp.dev/static/samples/json/examples.json' -H 'origin: https://ampbyexample-com.cdn.ampproject.org' -I

コマンドの結果に、正しいレスポンスヘッダーが示されます。

HTTP/2 200
access-control-allow-headers: Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token
access-control-allow-credentials: true
access-control-allow-origin: https://ampbyexample-com.cdn.ampproject.org
access-control-allow-methods: POST, GET, OPTIONS