バックエンドサーバを用いて認証する



  • この記事は、Google Sign-In for Android に関する記事を和訳したものです。
  • 原文: Authenticate with a backend server
  • 元記事のライセンスは CC-BYで、この和訳記事のライセンスは CC-BYです。
  • 自己責任でご利用ください。
  • 和訳した時期は 2019年6月ころです。

もしバックエンドサーバと通信を行うアプリあるいはサイトとともに Googleサインインを使用するならば、サーバ上で現在サインインしているユーザを識別する必要があるかもしれません。 それを安全に行うには、ユーザが正常にサインインした後、HTTPSを使用して、あなたのサーバにユーザの IDトークンを送信します。 それから、サーバー上で、IDトークンの整合性を検証し、セッションを確立する、あるいは、新しいアカウントを作成するために、トークンに含まれるユーザ情報を使用します。

あなたのサーバに IDトークンを送信する

まず、ユーザがサインインしたとき、彼らのIDトークンを取得します:

  1. Googleサインインを構成するとき、requestIdTokenメソッドを呼び出し、それにサーバのウェブクライアント IDを渡します。

    // Request only the user's ID token, which can be used to identify the
    // user securely to your backend. This will contain the user's basic
    // profile (name, profile picture URL, etc) so you should not need to
    // make an additional call to personalize your application.
    GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
            .requestIdToken(getString(R.string.server_client_id))
            .requestEmail()
            .build();
  2. あなたのアプリが起動したとき、silentSignInを呼び出すことによって、この端末あるいは他の端末上で、ユーザが既に Googleを使用してあなたのアプリにサインインをしているかどうかをチェックします。

    GoogleSignIn.silentSignIn()
        .addOnCompleteListener(
            this,
            new OnCompleteListener<GoogleSignInAccount>() {
              @Override
              public void onComplete(@NonNull Task<GoogleSignInAccount> task) {
                handleSignInResult(task);
              }
            });
  3. もしユーが暗黙的にサインインできなければ、あなたの通常のサインアウトされたエクスペリエンスを提示し、ユーザにサインインするオプションを与えます。 ユーザが サインインしたとき、サインインインテントのアクティビティの結果にて、ユーザの GoogleSignInAccountを取得します:

    // This task is always completed immediately, there is no need to attach an
    // asynchronous listener.
    Task<GoogleSignInAccount> task = GoogleSignIn.getSignedInAccountFromIntent(data);
    handleSignInResult(task);
  4. ユーザが暗黙的に、あるいは、明示的にサインインした後、GoogleSignInAccountオブジェクトから IDトークンを取得します:

    private void handleSignInResult(@NonNull Task<GoogleSignInAccount> completedTask) {
        try {
            GoogleSignInAccount account = completedTask.getResult(ApiException.class);
            String idToken = account.getIdToken();
    
            // TODO(developer): send ID Token to server and validate
    
            updateUI(account);
        } catch (ApiException e) {
            Log.w(TAG, "handleSignInResult:error", e);
            updateUI(null);
        }
    }

それから、HTTPS POSTリクエストを用いて、あなたのサーバに IDトークンを送信します:

HttpClient httpClient = new DefaultHttpClient();
HttpPost httpPost = new HttpPost("https://yourbackend.example.com/tokensignin");

try {
  List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(1);
  nameValuePairs.add(new BasicNameValuePair("idToken", idToken));
  httpPost.setEntity(new UrlEncodedFormEntity(nameValuePairs));

  HttpResponse response = httpClient.execute(httpPost);
  int statusCode = response.getStatusLine().getStatusCode();
  final String responseBody = EntityUtils.toString(response.getEntity());
  Log.i(TAG, "Signed in as: " + responseBody);
} catch (ClientProtocolException e) {
  Log.e(TAG, "Error sending ID token to backend.", e);
} catch (IOException e) {
  Log.e(TAG, "Error sending ID token to backend.", e);
}

IDトークンの整合性を確認する

HTTPS POSTによって IDトークンを受け取った後、トークンの整合性を検証しなければなりません。 トークンが有効であることを検証するには、次の基準が満たされていることを確認します:

  • IDトークンは、Googleによって正しく署名されています。 トークンの署名を検証するために、Googleの公開鍵(JWKあるいは PEM形式にて利用可能)を使用してください。 これらのキーは定期的にローテーションされます; それらを再び取得する必要があるときを決定するには、レスポンス内の Cache-Controlヘッダを調べてください。
  • IDトークンの audの値が、あなたのアプリのクライアント IDのひとつに等しい。 このチェックは、悪意のあるアプリに発行された IDトークンが、あなたのアプリのバックエンドサーバ上の、同じユーザについてのデータにアクセスするために使用されるために発行されることを防ぐために必要です。
  • IDトークンの issの値が、accounts.google.comあるいは https://accounts.google.comに等しい。
  • IDトークンの有効期限(exp)が経過していない。
  • もし G suiteドメインのメンバのみにアクセスを制限したければ、IDトークンが、あなたの G Suiteドメイン名と一致する hd句を持っていることを検証してください。

これらの検証ステップを実行するために独自のコードを書くのではなく、私たちは、あなたのプラットフォームのための Google APIクライアントライブラリ、あるいは、一般的な目的の JWTライブラリを使用することを強くお勧めします。 開発およびデバッグのために、私たちの tokeninfo検証エンドポイントを呼び出すことができます。

Google APIクライアントライブラリを使用する

本番環境にて Google IDトークンを検証するには、Google API Client Libraries (例えば、JavaNode.jsPHPPython)のいずれかを使用することが推奨される方法です。

Java

Javaにて IDトークンを検証するには、 GoogleIdTokenVerifierオブジェクトを使用します。 例えば:

import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken.Payload;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;

...

GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(transport, jsonFactory)
    // Specify the CLIENT_ID of the app that accesses the backend:
    .setAudience(Collections.singletonList(CLIENT_ID))
    // Or, if multiple clients access the backend:
    //.setAudience(Arrays.asList(CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3))
    .build();

// (Receive idTokenString by HTTPS POST)

GoogleIdToken idToken = verifier.verify(idTokenString);
if (idToken != null) {
  Payload payload = idToken.getPayload();

  // Print user identifier
  String userId = payload.getSubject();
  System.out.println("User ID: " + userId);

  // Get profile information from payload
  String email = payload.getEmail();
  boolean emailVerified = Boolean.valueOf(payload.getEmailVerified());
  String name = (String) payload.get("name");
  String pictureUrl = (String) payload.get("picture");
  String locale = (String) payload.get("locale");
  String familyName = (String) payload.get("family_name");
  String givenName = (String) payload.get("given_name");

  // Use or store profile information
  // ...

} else {
  System.out.println("Invalid ID token.");
}

GoogleIdTokenVerifier.verify()メソッドは、JWT署名、audクレーム、issクレーム、および expクレームを検証します。

もしあなたの G Suiteドメインのメンバのみにアクセスを制限したければ、Payload.getHostedDomain()メソッドによって返されるドメイン名をチェックすることによって、hd句も検証します。

Node.js

Node.jsにて IDトークンを検証するには、Google Auth Library for Node.jsを使用します。 ライブラリをインストールするには:

npm install google-auth-library --save
それから、verifyIdToken()関数を呼び出します。 例えば:

const {OAuth2Client} = require('google-auth-library');
const client = new OAuth2Client(CLIENT_ID);
async function verify() {
  const ticket = await client.verifyIdToken({
      idToken: token,
      audience: CLIENT_ID,  // Specify the CLIENT_ID of the app that accesses the backend
      // Or, if multiple clients access the backend:
      //[CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3]
  });
  const payload = ticket.getPayload();
  const userid = payload['sub'];
  // If request specified a G Suite domain:
  //const domain = payload['hd'];
}
verify().catch(console.error);

verifyIdTokenは JWT署名、aud句、exp句、および iss句を検証します。

もしあなたの G Suiteドメインのメンバのみにアクセスを制限したければ、hd句があなたの G Suiteドメイン名と一致することも検証します。

PHP

PHPにて IDトークンを検証するには、Google API Client Library for PHPを使用します。 ライブラリをインストールするには(例えば、Composerを使用します):

composer require google/apiclient
それから、verifyIdToken()関数を呼び出します。 例えば:

require_once 'vendor/autoload.php';

// Get $id_token via HTTPS POST.

$client = new Google_Client(['client_id' => $CLIENT_ID]);  // Specify the CLIENT_ID of the app that accesses the backend
$payload = $client->verifyIdToken($id_token);
if ($payload) {
  $userid = $payload['sub'];
  // If request specified a G Suite domain:
  //$domain = $payload['hd'];
} else {
  // Invalid ID token
}

verifyIdToken関数は、JWT署名、aud句、exp句、および iss句を検証します。

もしあなたの G Suiteドメインのメンバのみにアクセスを制限したければ、hd句があなたの G Suiteドメイン名と一致することも検証します。

Python

Pythonにて IDトークンを検証するには、verify_oauth2_tokenを使用します。 例えば:

from google.oauth2 import id_token
from google.auth.transport import requests

# (Receive token by HTTPS POST)
# ...

try:
    # Specify the CLIENT_ID of the app that accesses the backend:
    idinfo = id_token.verify_oauth2_token(token, requests.Request(), CLIENT_ID)

    # Or, if multiple clients access the backend server:
    # idinfo = id_token.verify_oauth2_token(token, requests.Request())
    # if idinfo['aud'] not in [CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3]:
    #     raise ValueError('Could not verify audience.')

    if idinfo['iss'] not in ['accounts.google.com', 'https://accounts.google.com']:
        raise ValueError('Wrong issuer.')

    # If auth request is from a G Suite domain:
    # if idinfo['hd'] != GSUITE_DOMAIN_NAME:
    #     raise ValueError('Wrong hosted domain.')

    # ID token is valid. Get the user's Google Account ID from the decoded token.
    userid = idinfo['sub']
except ValueError:
    # Invalid token
    pass

verify_oauth2_token関数は、JWT署名、aud句、および exp句を検証します。 verify_oauth2_tokenが返すオブジェクトを調べることによって、iss句、および hd句(該当する場合には)も検証しなければなりません。 もし複数のクライアントがバックエンドサーバにアクセスするならば、手動で aud句も検証します。

tokeninfoエンドポイントを呼び出す

デバッグ用に IDトークンを検証する簡単な方法は、tokeninfoエンドポイントを使用することです。 このエンドポイントを呼び出すことは、あなたのための検証のほとんどを行う追加のネットワークリクエストを発生させます、しかし、少しの待ち時間、および、ネットワークエラーの可能性を発生させます。

tokeninfoエンドポイントを使用して IDトークンを検証するには、エンドポイントに HTTPS POST、あるいは GETリクエストを行い、あなたの IDトークンを id_tokenパラメータに渡します。 例えば、トークン "XYZ123" を検証するには、次の GETリクエストを行います:

https://oauth2.googleapis.com/tokeninfo?id_token=XYZ123

もしトークンが適切に署名されていて、issおよび exp句が期待される値を持っているならば、HTTP 200レスポンスを取得するでしょう、それは、ボディが JSON形式の IDトークン句を含んでいます。 これは、レスポンスの例です:

{
 // These six fields are included in all Google ID Tokens.
 "iss": "https://accounts.google.com",
 "sub": "110169484474386276334",
 "azp": "1008719970978-hb24n2dstb40o45d4feuo2ukqmcc6381.apps.googleusercontent.com",
 "aud": "1008719970978-hb24n2dstb40o45d4feuo2ukqmcc6381.apps.googleusercontent.com",
 "iat": "1433978353",
 "exp": "1433981953",

 // These seven fields are only included when the user has granted the "profile" and
 // "email" OAuth scopes to the application.
 "email": "testuser@gmail.com",
 "email_verified": "true",
 "name" : "Test User",
 "picture": "https://lh4.googleusercontent.com/-kYgzyAWpZzJ/ABCDEFGHI/AAAJKLMNOP/tIXL9Ir44LE/s99-c/photo.jpg",
 "given_name": "Test",
 "family_name": "User",
 "locale": "en"
}

もし G Suiteの顧客であるならば、hd句にも興味を持つかもしれません、それは、ホストされたユーザのドメインを示しています。 これは、特定のドメインのメンバのみにリソースへのアクセスを制限するために使用されることができます。 この句の欠如は、ユーザが G Suiteのホストされたドメインに所属していないことを示します。

アカウントあるいはセッションを作成する

トークンを検証した後、ユーザが既にあなたのユーザデータベースに存在しているかどうかをチェックします。 もしそうであるならば、ユーザのための認証されたセッションを確立します。 もしまだユーザがあなたのユーザデータベースに存在していなければ、IDトークンのペイロード内の情報から新しいユーザのレコードを作成し、ユーザのためのセッションを確立します。 あなたのアプリにて新しく作成されたユーザを検知するときに必要とする追加のプロファイル情報をユーザに促すことができます。

Cross Account Protectionを用いてあなたのユーザのアカウントをセキュアにする

ユーザのサインインに Googleを頼るとき、Googleがユーザのデータを保護するために構築した、セキュリティ機能およびインフラのすべてから自動的に利益を得るでしょう。 しかしながら、万一、ユーザの Googleアカウントが危険にさらされる、あるいは、その他の重大なセキュリティイベントがある場合、あなたのアプリも、攻撃に対して脆弱になる可能性があります。 主要なセキュリティイベントからあなたのアカウントをより適切に保護するには、Googleからセキュリティアラートを受信するために Cross Account Protectionを使用します。 これらのイベントを受信したとき、ユーザの Googleアカウントのセキュリティへの重要な変更への可視性を得て、あなたのアカウントをセキュアにするために、あなたのサービス上でアクションをとることができます。