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 を処理する必要があります。処理しなければ、リクエストは失敗してしまいます。
ではどうすればいいですか?
- 動的データをフェッチする AMP ページの場合は、自分のドメインでだけでなく、キャッシュされたページも必ずテストしてください。(以下の「AMP での CORS のテスト」セクションを参照してください。)
- CORS リクエストとレスポンスの処理について、このドキュメントの指示に従ってください。
CORS リクエストに cookie を使用する
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 の制限が適用されます)。
サードパーティ cookie の制限
AMP でのクレデンシャル CORS リクエストには、ブラウザに指定されたサードパーティ cookie の制限が適用されます。これらの制限はブラウザやプラットフォームによって異なりますが、一部のブラウザでは、ユーザーが過去にファーストパーティ(トップ)ウィンドウでオリジンを訪問したことがなければ、オリジンが cookie を設定できなくなっています。言い換えると、ユーザーがオリジンのウェブサイトに直接訪問したことがある場合のみ、cookie が設定されるということになります。このため、CORS を介してアクセスされるサービスにおいて、デフォルトで cookie が設定されると考えてはいけません。
AMP における CORS セキュリティ
AMP ページのリクエストとレスポンスの有効性と安全性を確保するには、以下を行う必要があります。
バックエンドで Node を使用している場合は、AMP CORS ミドルウェアを使用できます。これは、AMP Toolbox の一部です。
CORS リクエストの検証
エンドポイントが CORS リクエストを受信したら、次の項目を行います。
- CORS
Origin
ヘッダーが許可されたオリジンであることを検証する(サイト運営者のオリジンと AMP キャッシュ)。 - 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
)
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
ヘッダーの値を検証してエコーする。
状態が変化するリクエストの処理
システムの状態を変更する可能性のあるリクエスト(ユーザーがメーリングリストを購読するか購読解除するなど)を処理する前に、以下のことを確認します。
Origin
ヘッダーが設定されている場合:
- オリジンが以下の値のいずれかに一致しない場合は、中断してエラーレスポンスを返します。
<publisher's domain>.cdn.ampproject.org
- サイト運営者のオリジン(あなたのオリジン)
*
は、ワイルドカード一致であり、実際のアスタリスク(*)ではありません。
- 含まれている場合は、リクエストを処理します。
Origin
ヘッダーが設定されていない場合:
- リクエストに
AMP-Same-Origin: true
ヘッダーが含まれているかどうかを確認します。このヘッダーが含まれていない場合は、中断してエラーレスポンスを返します。 - 一致する場合は、リクエストを処理します。
サンプルウォークスルー: CORS リクエストとレスポンスの処理
エンドポイントへの CORS リクエストで考えられるシナリオには 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 ページが正しく表示されて機能することを確認します。
- ブラウザで、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
- ブラウザの開発ツールを開き、エラーがないこととすべてのリソースが正しく読み込まれることを確認します。
サーバーのレスポンスヘッダーを確認する
サーバーが正しい 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