🚗

【SDV時代の認証UX】Android for Carsの認証をスマホで完結させる仕組みを作ってみた

に公開

はじめに

自動車メーカーでモバイルアプリやSDV(Software Defined Vehicle)ソフトウェアの開発に従事しているエンジニアです。

突然ですが、車のナビ画面でGoogleアカウントにログインしようとして、あのちっちゃいリモコンでメールアドレスをポチポチ入力した経験、ありませんか?

「なんでやねん!スマホあるやろ!」

...と、心の中のすち子が叫んだのは私だけではないはずです。

というわけで今回は、Android for Cars(Android Automotive OS)の認証を、手持ちのスマートフォンで完結させる仕組みを作って検証してみました。しかも Google だけでなく Azure AD External ID(Microsoft Entra External ID) にも対応させて、企業ユースケースも視野に入れています。

この記事の目的

SDVソフトウェア開発において、認証認可とセキュリティは避けて通れないテーマです。本記事では以下を検証します。

  • OAuth 2.0 Device Authorization Flow は車載システムで実用的か
  • マルチIDプロバイダー(Google + Azure AD)対応は現実的か
  • セキュリティと利便性のバランスはどう取るべきか

システム概要

解決したい課題

車載システムでの認証には、以下のような課題があります。

課題 詳細
入力の困難さ 物理キーボードがない、タッチ操作も運転中は危険
セキュリティ 車内での認証情報入力は覗き見リスクがある
UX 認証のためだけに車を停める必要がある

解決アプローチ:Device Authorization Flow

OAuth 2.0 には「Device Authorization Grant」(RFC 8628)という、まさにこのユースケースのために設計された仕様があります。

┌─────────────────┐                              ┌─────────────────┐
│  車載システム     │                              │  スマートフォン   │
│ (Android Auto)  │                              │                 │
└────────┬────────┘                              └────────┬────────┘
         │                                                │
         │ 1. QRコード表示                                 │
         │    「スマホでスキャンしてね」                     │
         │                                                │
         │ ═══════════════════════════════════════════════│
         │           📱 QRスキャン                         │
         │ ═══════════════════════════════════════════════│
         │                                                │
         │                                    2. Google/Microsoft
         │                                       ログイン画面へ
         │                                                │
         │                                    3. 普段通りログイン
         │                                       (指紋認証もOK)
         │                                                │
         │ 4. 自動的にログイン完了!                        │
         │    (ポーリングで検知)                          │
         ▼                                                ▼

要するに、車はQRコードを見せるだけ。認証はスマホでやる。 シンプルですね。

技術スタック

車載アプリ(Android Automotive)

// 使用技術
- Kotlin 1.9.x
- Jetpack Compose for Automotive
- Hilt (DI)
- Retrofit + OkHttp
- ZXing (QRコード生成)

スマートフォンアプリ

// 使用技術
- Kotlin 1.9.x
- Jetpack Compose
- CameraX + ML Kit (QRスキャン)
- Chrome Custom Tabs
- Hilt (DI)

実装のポイント

1. プロバイダー抽象化

Google と Azure AD の両方に対応するため、Strategy Pattern でプロバイダーを抽象化しました。

interface AuthProvider {
    val providerId: String
    val displayName: String
    val iconResId: Int
    val brandColor: Color
    
    suspend fun requestDeviceCode(): Result<DeviceCodeResponse>
    suspend fun pollToken(deviceCode: String): Result<TokenResponse>
    fun buildQRCodeContent(response: DeviceCodeResponse): String
}

これにより、将来的に Apple ID など他のプロバイダーを追加する際も、新しい実装クラスを追加するだけで済みます。

// Google実装
class GoogleAuthProvider : AuthProvider {
    override val providerId = "google"
    private val deviceCodeUrl = "https://oauth2.googleapis.com/device/code"
    // ...
}

// Azure AD External ID実装
class AzureAuthProvider : AuthProvider {
    override val providerId = "azure"
    private val deviceCodeUrl = "https://{tenant}.ciamlogin.com/{tenantId}/oauth2/v2.0/devicecode"
    // ...
}

2. QRコードのフォーマット設計

車載システムとスマホアプリ間の通信はQRコードで行います。以下のカスタムURLスキームを定義しました。

carauth://auth?provider=google&url=https://...&code=XXXX-XXXX
パラメータ 説明 例
provider IDプロバイダー google, azure
url 認証URL https://www.google.com/device
code ユーザーコード GQVQ-JKEC

3. Google vs Azure AD のAPI差異

両者のDevice Authorization Flowは基本的に同じですが、微妙な差異があります。

項目 Google Azure AD
Device Code URL oauth2.googleapis.com/device/code {tenant}.ciamlogin.com/.../devicecode
認証URLのキー名 verification_url verification_uri
ユーザー認証ページ google.com/device microsoft.com/devicelogin
コード形式 XXXX-XXXX XXXXXXXX
// Azure ADのレスポンスパース時の注意点
private fun parseAzureDeviceCodeResponse(json: String): DeviceCodeResponse {
    val jsonObject = JSONObject(json)
    return DeviceCodeResponse(
        deviceCode = jsonObject.getString("device_code"),
        userCode = jsonObject.getString("user_code"),
        // Azureは verification_uri を使用
        verificationUrl = jsonObject.getString("verification_uri"),
        expiresIn = jsonObject.getInt("expires_in"),
        interval = jsonObject.getInt("interval")
    )
}

4. トークンポーリングの実装

認証完了を検知するため、車載システム側でトークンエンドポイントをポーリングします。

class DeviceAuthManager(
    private val provider: AuthProvider
) {
    suspend fun pollForToken(
        deviceCode: String,
        interval: Long,
        expiresIn: Int
    ): Result<TokenResponse> {
        val startTime = System.currentTimeMillis()
        val expiresAt = startTime + (expiresIn * 1000L)
        
        while (System.currentTimeMillis() < expiresAt) {
            val result = provider.pollToken(deviceCode)
            
            when {
                result.isSuccess -> return result
                
                result.exceptionOrNull()?.message == "authorization_pending" -> {
                    // まだ認証中、待機して再試行
                    delay(interval)
                }
                
                result.exceptionOrNull()?.message == "slow_down" -> {
                    // レート制限、間隔を延長
                    delay(interval * 2)
                }
                
                else -> return result // その他のエラー
            }
        }
        
        return Result.failure(AuthException("認証がタイムアウトしました"))
    }
}

セキュリティ考慮事項

実装したセキュリティ対策

対策 実装方法
トークン暗号化保存 EncryptedSharedPreferences 使用
通信の暗号化 TLS 1.3 必須
証明書ピニング OkHttp CertificatePinner
短寿命トークン Access Token は1時間で失効
// トークンの安全な保存
val masterKey = MasterKey.Builder(context)
    .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
    .build()

val encryptedPrefs = EncryptedSharedPreferences.create(
    context,
    "auth_prefs",
    masterKey,
    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)

Azure AD External ID 固有の考慮事項

Azure AD を使用する場合、追加のセキュリティ機能が利用できます。

  • 条件付きアクセス: デバイスコンプライアンス要求、位置情報ベースの制御
  • リスクベース認証: 不審なサインインの検出と追加認証要求
  • テナント制限: 許可されたテナントのみ認証可能
// 許可テナントの検証
val allowedTenants = listOf(
    "your-company-tenant-id",
    "partner-company-tenant-id"
)

fun validateTenant(idToken: String): Boolean {
    val claims = decodeJwt(idToken)
    val tenantId = claims["tid"] as String
    return tenantId in allowedTenants
}

検証結果

動作確認環境

項目 環境
車載システム Android Automotive OS エミュレータ (API 33)
スマートフォン Pixel 7 (Android 14)
IDプロバイダー Google OAuth, Azure AD External ID

検証シナリオと結果

正常系

シナリオ 結果 所要時間
Google認証(QRスキャン → ログイン) 成功 約15秒
Azure AD認証(QRスキャン → ログイン) 成功 約20秒
生体認証(指紋)でのログイン 成功 約10秒

異常系

シナリオ 期待動作 結果
認証タイムアウト(30分放置) エラー表示 + 再試行ボタン 正常
ユーザーが認証を拒否 エラー表示 正常
ネットワーク切断 リトライ処理 正常
不正なQRコード エラーメッセージ表示 正常

パフォーマンス

  • QRコード生成: 約50ms
  • ポーリング間隔: 5秒(Google/Azure AD推奨値)
  • メモリ使用量: 車載アプリ約80MB、スマホアプリ約60MB

実用性の評価

実用的と判断できる点

  1. UXの大幅改善: 車載画面での文字入力が不要
  2. セキュリティ向上: スマホの生体認証が使える
  3. 標準仕様準拠: RFC 8628 に準拠、主要IDプロバイダーがサポート
  4. 拡張性: 新しいプロバイダーの追加が容易

課題・検討事項

  1. オフライン非対応: インターネット接続が必須
  2. スマホ必須: スマホを持っていない/忘れた場合の代替手段が必要
  3. QRコード読み取り距離: 車内の照明条件によっては読み取りにくい場合も

改善案

将来的な拡張として検討中:
- Bluetooth LE によるデバイス間通信(QRコード不要化)
- NFC タップでの認証開始
- 音声アシスタント連携(「OK Google, ログインして」)

おわりに

Device Authorization Flow を使った車載システムの認証連携は、十分に実用的であることが確認できました。

特に Azure AD External ID との組み合わせは、企業向け車両管理や社用車のアカウント管理など、B2B ユースケースでの活用が期待できます。

SDV時代において、認証・認可の仕組みは車両セキュリティの根幹を担う重要な要素です。今後も新しい標準仕様やベストプラクティスをウォッチしながら、より安全で便利な車載システムの実現を目指していきたいと思います。

「ドリルすんのかい、せんのかい」 ではなく、 「ログインすんのかい、せんのかい」 で悩まなくて済む未来がすぐそこに来ています。

...すみません、最後まですち子ネタを入れたかっただけです。
需要があればプロジェクト一式どっかで公開する事も検討します

参考資料

Discussion