ウェアラブルでの認証: 認証情報マネージャー

Wear OS アプリはコンパニオン アプリがなくてもスタンドアロンで動作できます。つまり、Wear OS アプリは、インターネットからデータにアクセスする際、独自に認証を管理する必要があります。しかし、スマートウォッチは画面サイズが小さく、入力機能が限定されているため、Wear OS アプリで使用できる認証オプションは限られています。

このガイドでは、Wear OS アプリで推奨される認証方法である認証情報マネージャーの手順について説明します。

優れたログイン エクスペリエンスを設計する方法について詳しくは、ログイン UX ガイドをご覧ください。

事前検討事項

実装を開始する前に、次の点を考慮してください。

ゲストモード

すべての機能で認証を求めるのではなく、ログインを必要としない機能をできるだけ多くユーザーに提供してください。

モバイルアプリを使用したことがないユーザーが Wear アプリを見つけてインストールする場合があります。そうしたユーザーはアカウントを持っていないことや、アカウントで提供される機能を知らないことがあります。ゲストモード機能によってアプリの機能が正確に紹介されることを確認してください。

デバイスによっては、ロック解除状態が長く続くことがあります

Wear OS 5 以降を搭載するサポート対象デバイスでは、ユーザーがデバイスを手首に装着しているかどうかが検出されます。ユーザーが手首検出をオフにして、デバイスを手首から外すと、通常よりも長い時間デバイスのロックが解除されたままになります。

機密情報や個人情報の表示など、アプリでより高いレベルのセキュリティが必要な場合は、まず手首検出が有効になっているかどうかを確認します。

val wristDetectionEnabled =
        isWristDetectionAutoLockingEnabled(applicationContext)

このメソッドの戻り値が false の場合、ユーザー固有のコンテンツを表示する前に、アプリのアカウントにログインするようユーザーに求めるメッセージを表示します。

認証情報マネージャー

Wear OS での認証には、認証情報マネージャーを使用することをおすすめします。ユーザーは、ペア設定されたスマートフォンを接続することなく、パスワードを覚えることなく、スタンドアロン設定で Wear OS アプリにログインできる、より安全な環境を利用できます。

このドキュメントでは、ホストする標準認証メカニズムを使用して認証情報マネージャー ソリューションを実装するためにデベロッパーが知っておく必要のある情報を概説します。認証メカニズムには次のようなものがあります。

  • パスキー
  • パスワード
  • フェデレーション ID(「Google でログイン」など)

このガイドでは、認証情報マネージャーのバックアップとして、他の Wear OS 認証方法(データレイヤ トークン共有OAuth)を移行する方法と、非推奨となったスタンドアロンの Google ログイン ボタンから埋め込みの認証情報マネージャー バージョンへの移行を処理するための特別な手順についても説明します。

Wear OS のパスキー

デベロッパーは、Wear OS 認証情報マネージャーの実装にパスキーを実装することを強くおすすめします。パスキーはエンドユーザー認証の新しい業界標準であり、ユーザーにいくつかの重要なメリットをもたらします。

パスキーの方が簡単

  • ユーザーは、ログインに使用するアカウントを選択できます。ユーザー名を入力する必要はありません。
  • ユーザーはデバイスの画面ロックを使用して認証できます。
  • パスキーを作成して登録すると、ユーザーは新しいデバイスにシームレスに切り替えて、再登録することなくすぐに使用できます。

パスキーは安全です

  • デベロッパーはパスワードを保存するのではなく、公開鍵のみをサーバーに保存します。つまり、攻撃者がサーバーをハッキングする価値が大幅に低下し、侵害が発生した場合のクリーンアップ作業も大幅に軽減されます。
  • パスキーは、フィッシング対策に有効な保護機能を提供します。パスキーは登録したウェブサイトやアプリでのみ機能します。ブラウザまたは OS が確認を処理するため、ユーザーが欺かれて偽のサイトで認証を行うことはありません。
  • パスキーを使用すると SMS を送信する必要がなくなり、認証の費用対効果が向上します。

パスキーを実装する

すべての実装タイプに関する設定とガイダンスが含まれています。

設定

  1. アプリ モジュールの build.gradle ファイルで、ターゲット API レベルを 35 に設定します。

    android {
        defaultConfig {
            targetSdkVersion(35)
        }
    }
    
  2. androidx.credentials リリース リファレンスの最新の安定版を使用して、アプリまたはモジュールの build.gradle ファイルに次の行を追加します。

    androidx.credentials:credentials:1.5.0
    androidx.credentials:credentials-play-services-auth:1.5.0
    

組み込みの認証方法

認証情報マネージャーは統合 API であるため、Wear OS の実装手順は他のデバイスタイプと同じです。

モバイル向けの手順を使用して、パスキーとパスワードのサポートの実装を開始します。

認証情報マネージャーに「Google でログイン」のサポートを追加する手順はモバイル開発を対象としていますが、Wear OS でも同じ手順です。このケースの特別な考慮事項については、従来の「Google でログイン」からの移行のセクションをご覧ください。

Wear OS では認証情報を作成できないため、モバイル デバイスの手順で説明されている認証情報の作成方法を実装する必要はありません。

バックアップ認証方法

Wear OS アプリで使用できる認証方法は他に 2 つあります。OAuth 2.0(いずれかのバリエーション)とモバイル認証トークン データレイヤ共有です。これらのメソッドには Credential Manager API との統合ポイントはありませんが、ユーザーが認証情報マネージャーの画面を閉じた場合に備えて、認証情報マネージャーの UX フローにフォールバックとして含めることができます。

認証情報マネージャー画面を閉じるユーザー操作を処理するには、GetCredential ロジックの一部として NoCredentialException をキャッチし、独自のカスタム認証 UI に移動します。

yourCoroutineScope.launch {
    try {
      val response = credentialManager.getCredential(activity, request)
      signInWithCredential(response.credential)
    } catch (e: GetCredentialCancellationException) {
      navigateToFallbackAuthMethods()
    }
}

カスタム認証 UI では、ログイン UX ガイドで説明されている他の認証方法を指定できます。

データレイヤ トークンの共有

スマートフォンのコンパニオン アプリは、Wearable Data Layer API を使用して認証データを Wear OS アプリに安全に転送できます。認証情報をメッセージまたはデータアイテムとして転送します。

この種の認証では通常、ユーザーによる操作は必要ありません。ただし、ログイン中であることをユーザーに通知せずに認証を行うことは避けてください。閉じることができる画面を使用して、アカウントがモバイルから移行されることをユーザーに通知できます。

重要: このオプションは、対応するモバイルアプリがインストールされている場合に、Android とペア設定されたスマートウォッチでしか機能しないため、Wear OS アプリは他の認証方法を少なくとも 1 つ提供する必要があります。対応するモバイルアプリを持っていないユーザーや、Wear OS デバイスが iOS デバイスとペア設定されているユーザー向けに、代替の認証方法を提供してください。

次の例に示すように、モバイルアプリからデータレイヤを使用してトークンを渡します。

val token = "..." // Auth token to transmit to the Wear OS device.
val dataClient: DataClient = Wearable.getDataClient(context)
val putDataReq: PutDataRequest = PutDataMapRequest.create("/auth").run {
    dataMap.putString("token", token)
    asPutDataRequest()
}
val putDataTask: Task<DataItem> = dataClient.putDataItem(putDataReq)

次の例に示すように、Wear OS アプリのデータ変更イベントをリッスンします。

val dataClient: DataClient = Wearable.getDataClient(context)
dataClient.addListener{ dataEvents ->
    dataEvents.forEach { event ->
        if (event.type == DataEvent.TYPE_CHANGED) {
            val dataItemPath = event.dataItem.uri.path ?: ""
            if (dataItemPath.startsWith("/auth")) {
                val token = DataMapItem.fromDataItem(event.dataItem).dataMap.getString("token")
                // Display an interstitial screen to notify the user that
                // they're being signed in.
                // Then, store the token and use it in network requests.
            }
        }
    }
}

ウェアラブル データレイヤの使用方法について詳しくは、Wear OS でのデータの送信と同期をご覧ください。

OAuth 2.0 を使用する

Wear OS は 2 つの OAuth 2.0 ベースのフローをサポートしています。これらについては、この後のセクションで説明します。

  • RFC 7636 で定義されている、Proof Key for Code Exchange(PKCE)による Authorization Code Grant
  • RFC 8628 で定義されている Device Authorization Grant(DAG)
Proof Key for Code Exchange(PKCE)

PKCE を効果的に使用するには、RemoteAuthClient を使用します。次に、Wear OS アプリから OAuth プロバイダに認証リクエストを実行するには、OAuthRequest オブジェクトを作成します。このオブジェクトは、トークンを取得するための OAuth エンドポイントへの URL と CodeChallenge オブジェクトで構成されます。

次のコードは、認証リクエストの作成例を示しています。

val request = OAuthRequest.Builder(this.applicationContext)
    .setAuthProviderUrl(Uri.parse("https://...."))
    .setClientId(clientId)
    .setCodeChallenge(codeChallenge)
    .build()

認証リクエストを作成したら、sendAuthorizationRequest() メソッドを使用してコンパニオン アプリに送信します。

val client = RemoteAuthClient.create(this)
client.sendAuthorizationRequest(request,
    { command -> command?.run() },
    object : RemoteAuthClient.Callback() {
        override fun onAuthorizationResponse(
            request: OAuthRequest,
            response: OAuthResponse
        ) {
            // Extract the token from the response, store it, and use it in
            // network requests.
        }

        override fun onAuthorizationError(errorCode: Int) {
            // Handle any errors.
        }
    }
)

このリクエストによってコンパニオン アプリの呼び出しがトリガーされ、ユーザーのスマートフォンのウェブブラウザに認証 UI が表示されます。OAuth 2.0 プロバイダがユーザーを認証し、リクエストされた権限についてユーザーの同意を得ます。レスポンスは、自動生成されたリダイレクト URL に送信されます。

認証が成功または失敗した後、OAuth 2.0 サーバーは、リクエストで指定された URL にリダイレクトします。ユーザーがアクセス リクエストを承認すると、レスポンスに認証コードが格納されます。ユーザーがリクエストを承認しないと、レスポンスにエラー メッセージが格納されます。

レスポンスはクエリ文字列の形式を取り、次の例のようになります。

  https://wear.googleapis.com/3p_auth/com.your.package.name?code=xyz
  https://wear.googleapis-cn.com/3p_auth/com.your.package.name?code=xyz

これにより、ユーザーをコンパニオン アプリに誘導するページが読み込まれます。コンパニオン アプリはレスポンス URL を検証し、onAuthorizationResponse API を使用して Wear OS アプリにレスポンスを中継します。

スマートウォッチ アプリは、認証コードをアクセス トークンと交換できます。

Device Authorization Grant

Device Authorization Grant を使用する場合、ユーザーは別のデバイスで検証 URI を開きます。次に、認証サーバーはリクエストを承認または拒否するよう求めます。

このプロセスを簡素化するには、次の例に示すように、RemoteActivityHelper を使用して、ユーザーのペア設定されたモバイル デバイスでウェブページを開きます。

// Request access from the authorization server and receive Device Authorization
// Response.
val verificationUri = "..." // Extracted from the Device Authorization Response.
RemoteActivityHelper.startRemoteActivity(
    this,
    Intent(Intent.ACTION_VIEW)
        .addCategory(Intent.CATEGORY_BROWSABLE)
        .setData(Uri.parse(verificationUri)),
    null
)
// Poll the authorization server to find out if the user completed the user
// authorization step on their mobile device.

iOS アプリの場合は、トークンの認証にブラウザを使用するのではなく、ユニバーサル リンクを使用して、アプリでこのインテントをインターセプトします。

以前の「Google でログイン」からの移行

認証情報マネージャーには、「Google でログイン」ボタン専用の統合ポイントがあります。以前は、このボタンをアプリの認証 UX の任意の場所に追加できましたが、認証情報マネージャーに追加されたため、古いオプションは非推奨になりました。

// Define a basic SDK check.
fun isCredentialManagerAvailable(): Boolean {
 return android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM
}

// Elsewhere in the code, use it to selectively disable the legacy option.
Button(
  onClick = {
    if (isCredentialManagerAvailable()) {
      Log.w(TAG, "Devices on API level 35 or higher should use
                  Credential Manager for Sign in with Google")
    } else {
      navigateToSignInWithGoogle()
    }},
  enabled = !isCredentialManagerAvailable(),
  label = { Text(text = stringResource(R.string.sign_in_with_google)) },
  secondaryLabel = { Text(text = "Disabled on API level 35+")
  }
)