HTTP リクエスト ヘッダーを追加する

HTTP リクエストには、User-Agent や Content-Type などのヘッダーが含まれます。ブラウザによって付加されるヘッダー以外に、Android アプリは EXTRA_HEADERS Intent エクストラを介して Cookie や Referrer などの追加ヘッダーを追加できます。セキュリティ上の理由から、Chrome はインテントの起動方法と起動場所に応じて、一部の追加ヘッダーをフィルタします。

クロスオリジン リクエストでは、クライアントとサーバーが同じ当事者によって所有されていないため、追加のセキュリティ レイヤが必要です。このガイドでは、Chrome のカスタムタブを介してこのようなリクエストを起動する方法について説明します。つまり、ブラウザタブで URL を開くアプリから起動されるインテントについて説明します。Chrome 83 までは、デベロッパーはカスタムタブを起動する際に任意のヘッダーを追加できました。バージョン 83 以降、Chrome は approvelisted 以外のすべてのクロスオリジン ヘッダーをフィルタリングするようになりました。これは、approvelisted 以外のヘッダーがセキュリティ リスクをもたらすためです。Chrome 86 以降では、サーバーとクライアントがデジタル アセットリンクを使用して関連付けられている場合、承認リストに登録されていないヘッダーをクロスオリジン リクエストに付加できます。この動作を次の表にまとめます。

Chrome のバージョン 許可される CORS ヘッダー
Chrome 83 より前 approvelisted、non-approvelisted
Chrome 83 ~ Chrome 85 許可リストに登録済み
Chrome 86 以降 デジタル アセット リンクが設定されている場合の approvelisted、non-approvelisted

表 1.: 承認リストに登録されていない CORS ヘッダーのフィルタリング。

この記事では、サーバーとクライアント間の検証済み接続を設定し、それを使用して承認リストに登録された HTTP ヘッダーと承認リストに登録されていない HTTP ヘッダーの両方を送信する方法について説明します。コードについては、カスタムタブ インテントにヘッダーを追加するにスキップしてください。

背景

承認済みと非承認の CORS リクエスト ヘッダー

クロスオリジン リソース シェアリング(CORS)により、あるオリジンのウェブ アプリケーションが別のオリジンのリソースをリクエストできます。CORS-approvelisted ヘッダーのリストは、HTML 標準で管理されています。次の表に、承認リストに登録されたヘッダーの例を示します。

ヘッダー 説明
accept-language クライアントが理解できる自然言語をアドバタイズします
content-language 現在の読者を対象とした言語を説明する
content-type リソースのメディアタイプを示します

表 2.: 承認済みの CORS ヘッダーの例。

承認リストに登録されたヘッダーは、機密性の高いユーザー情報を含まず、サーバーが有害な可能性のあるオペレーションを実行する可能性が低いため、安全と見なされます。

承認リストに登録されていないヘッダーの例を次の表に示します。

ヘッダー 説明
bearer-token サーバーでクライアントを認証する
origin リクエストの送信元を示します
Cookie サーバーによって設定された Cookie が含まれています

表 3.: 承認リストに登録されていない CORS ヘッダーの例。

HTML 標準では、承認リストに登録されていないヘッダーを CORS リクエストに付加することは推奨されていません。サーバーは、クロスオリジン リクエストに承認リストに登録されているヘッダーのみが含まれていることを前提としています。クロスオリジン ドメインから承認リストに登録されていないヘッダーを送信すると、悪意のあるサードパーティ製アプリが、Chrome(または別のブラウザ)が保存してリクエストに付加するユーザー Cookie を不正使用するヘッダーを作成できるようになります。Cookie は、通常は不可能な悪意のあるサーバー トランザクションを認証する可能性があります。

CORS 許可リストに登録されたヘッダーをカスタムタブ リクエストに付加する

カスタムタブは、カスタマイズされたブラウザタブでウェブページを開くための特別な方法です。カスタムタブ インテントは CustomTabsIntent.Builder() を使用して作成できます。Browser.EXTRA_HEADERS フラグを指定した Bundle を使用して、これらのインテントにヘッダーを付加することもできます。

CustomTabsIntent intent = new CustomTabsIntent.Builder(session).build();

Bundle headers = new Bundle();
headers.putString("bearer-token", "Some token");
headers.putString("redirect-url", "Some redirect url");   
intent.intent.putExtra(Browser.EXTRA_HEADERS, headers);

intent.launchUrl(Activity.this, Uri.parse("http://www.google.com"));

承認リストに登録されたヘッダーは、常にカスタムタブの CORS リクエストに付加できます。ただし、Chrome はデフォルトで承認リストに登録されていないヘッダーをフィルタします。他のブラウザでは動作が異なる可能性がありますが、一般的に、承認リストに登録されていないヘッダーはブロックされることを想定しておく必要があります。

カスタムタブに承認リストにないヘッダーを含めるには、まずデジタル アクセスリンクを使用してクロスオリジン接続を検証します。次のセクションでは、これらの設定を行い、必要なヘッダーを使用してカスタムタブ インテントを起動する方法について説明します。

カスタムタブ インテントにヘッダーを追加する

承認リストに登録されていないヘッダーをカスタムタブ インテントで渡すには、Android アプリとウェブ アプリケーションの間にデジタル アセットリンクを設定し、作成者が両方のアプリケーションを所有していることを確認する必要があります。

公式ガイドに沿って、デジタル アセット リンクを設定します。リンク関係には "delegate_permission/common.use_as_origin" を使用します。これは、リンクが検証されると、両方のアプリが同じオリジンに属することを示します。

追加のヘッダーを含むカスタムタブ インテントを作成する

カスタムタブ インテントを作成する方法は複数あります。androidX で利用可能なビルダーを使用するには、ライブラリをビルドの依存関係に追加します。

implementation 'androidx.browser:browser:1.2.0'

インテントをビルドして、追加のヘッダーを追加します。

CustomTabsIntent constructExtraHeadersIntent(CustomTabsSession session) {
    CustomTabsIntent intent = new CustomTabsIntent.Builder(session).build();

    // Example non-cors-approvelisted headers.
    Bundle headers = new Bundle();
    headers.putString("bearer-token", "Some token");
    headers.putString("redirect-url", "Some redirect url");
    intent.intent.putExtra(Browser.EXTRA_HEADERS, headers);
    return intent;
}

カスタムタブ接続は、アプリと Chrome タブの間に CustomTabsSession を設定するために使用されます。アプリとウェブアプリが同じオリジンに属していることを確認するために、セッションが必要です。デジタル アセット リンクが正しく設定されている場合にのみ、検証に合格します。

CustomTabsClient.warmup() を呼び出すことをおすすめします。これにより、ブラウザ アプリケーションをバックグラウンドで事前初期化し、URL を開くプロセスを高速化できます。

// Set up a connection that warms up and validates a session.
CustomTabsServiceConnection connection = new CustomTabsServiceConnection() {
    @Override
    public void onCustomTabsServiceConnected(@NonNull ComponentName name, 
        @NonNull CustomTabsClient client) {
        // Create session after service connected.
        mSession = client.newSession(callback);
        client.warmup(0);
        // Validate the session as the same origin to allow cross origin headers.
        mSession.validateRelationship(CustomTabsService.RELATION_USE_AS_ORIGIN, 
            Uri.parse(url), null);
    }
    @Override
    public void onServiceDisconnected(ComponentName componentName) { }
};

検証後にインテントを起動するコールバックを設定する

CustomTabsCallback がセッションに渡されました。オリジン検証が成功したら、以前に作成した CustomTabsIntent を起動するように onRelationshipValidationResult() を設定します。

// Set up a callback that launches the intent after session validated.
CustomTabsCallback callback = new CustomTabsCallback() {
    @Override
    public void onRelationshipValidationResult(int relation, @NonNull Uri requestedOrigin, 
        boolean result, @Nullable Bundle extras) {
        // Launch custom tabs intent after session was validated as the same origin.
        CustomTabsIntent intent = constructExtraHeadersIntent(mSession);
        intent.launchUrl(MainActivity.this, Uri.parse(url));
    }
};

カスタムタブ サービス接続をバインドする

サービスをバインドすると、サービスが起動し、接続の onCustomTabsServiceConnected() が最終的に呼び出されます。サービスを適切にバインド解除することを忘れないでください。バインドとバインド解除は、通常、onStart() と onStop() のアクティビティのライフサイクル メソッドで行われます。

// Bind the custom tabs service connection.
// Call this in onStart()
CustomTabsClient.bindCustomTabsService(this,
    CustomTabsClient.getPackageName(MainActivity.this, null), connection);

// …
// Unbind the custom tabs service.
// Call this in onStop().
unbindService(connection);

デモ アプリケーション コード

カスタムタブ サービスについて詳しくは、こちらをご覧ください。動作するサンプルアプリについては、android-browser-helper GitHub リポジトリをご覧ください。

概要

このガイドでは、カスタムタブの CORS リクエストに任意のヘッダーを追加する方法について説明しました。承認済みのヘッダーは、すべてのカスタムタブの CORS リクエストに付加できます。承認リストに登録されていないヘッダーは、一般的に CORS リクエストで安全でないとみなされ、Chrome ではデフォルトでフィルタされます。アタッチできるのは、デジタル アセットリンクで検証された同じオリジンのクライアントとサーバーのみです。