Zenn
🔐

Kotlin & Jetpack Compose で AppAuth を使って OAuth2 ログインを実装する

2025/03/21に公開

公式ドキュメントが Java で書かれていたり、参考例を探すと大体 Activity とか Fragment を使うやつだったのでとりあえず Android アプリ開発の初心者である自分が Jetpack Compose で実装してみたログを書いときます。

AppAuth の公式リポジトリはこちら。
Kotlin じゃなくて Java だけどサンプルコード付きのドキュメントも書いてあります。
https://github.com/openid/AppAuth-Android

インストール

build.gradle.kts(:app) の dependencies 内に以下のコードを追加します。

    implementation(libs.appauth)

あとは画面の上の方に出てくる Sync ボタンを押してインストールおわり。

準備

Issuer (OAuth 認証提供者?) の情報を取得する方法はいくつかありますが、今回は公式ドキュメントの一番最初に出てくる authorization endpoint と token endpoint を直接指定するスタイルにします。
といってもどの Issuer を使うかで指定する値が変わるので良い感じに設定してください。

必要な情報は以下の通りです。

  • OAuth Client ID
  • Authorization Endpoint (URL)
  • Token Endpoint (URL)
  • Redirect URI (認証後にリダイレクトさせたい Web サービスの URL)
// すべて String
const val CLIENT_ID = "..."
const val AUTHORIZATION_ENDPOINT = "..."
const val TOKEN_ENDPOINT = "..."
const val REDIRECT_URI = "..."

コード例の中で唐突に謎のクラスが出てきた時は、特筆されてない限り AppAuth のものです。
うまくインストールできていればコード補完から import できるはず。

とりあえずまずはリクエスト作成に必要な AuthorizationServiceConfiguration を作成します。

val serviceConfig = AuthorizationServiceConfiguration(
    AUTHORIZATION_ENDPOINT.toUri(),
    TOKEN_ENDPOINT.toUri()
)

次に、リクエストビルダを作成します。

val authRequestBuilder = AuthorizationRequest.Builder(
    serviceConfig,
    CLIENT_ID,
    ResponseTypeValues.CODE,
    REDIRECT_URI.toUri()
)

あとで Intent (OAuth プロバイダのログインページを表示させるやつ)を取得する為に AuthorizationService が必要になりますが、作成時に context を渡す必要があるのでとりあえず lateinit で宣言だけしといて設定は後回しにします。

lateinit var authService: AuthorizationService

リクエストの作成と認証用 Intent の作成方法は以下の通りです。

fun createAuthorizationRequest(): AuthorizationRequest {
    val authRequest = authRequestBuilder
        // 必要なスコープを設定
        .setScopes(Scope.PROFILE, Scope.OPENID)
        .build()

    return authRequest
}

fun getAuthorizationIntent(context: Context, authRequest: AuthorizationRequest): Intent {
    authService = AuthorizationService(context)

    val authIntent = authService.getAuthorizationRequestIntent(authRequest)

    return authIntent
}

最後に、認証後にリダイレクトされた時にちゃんとアプリが起動されるように AppLinks の設定を行います。
まず、AndroidManifest.xml の <application> の中、既存の <activitiy>...</activity> の下にこれを追加します。
SCHEME, DOMAIN と PATH はそれぞれリダイレクト先 URL に設定した URL のものを良い感じに書き直してください。
SCHEME は http か https のどちらかです。
DOMAIN にはスラッシュを含めず、PATH はスラッシュから始めてください。

例: https://example.com/callback にリダイレクトさせたい → SCHEME: https, DOMAIN: example.com, PATH: /callback

<activity
    android:name="net.openid.appauth.RedirectUriReceiverActivity"
    android:exported="true"
    tools:node="replace">
    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW" />

        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />

        <data android:scheme="SCHEME" />
        <data android:host="DOMAIN" />
        <data android:pathPattern="PATH" />
    </intent-filter>
</activity>

次に、Android Studio のメニューボタンを押して Tools → App Links Assistant を開きます。

この画面の左上にある「Create Applink」をクリックして、開かれた画面にある ③ の「Open Digital Asset Links File Generator」をクリックします。

そして開かれた画面にある「Generate Digital Asset Links File」をクリックして、出てきたテキストをコピー(または Save ボタンを押して保存)し、Web サービスの /.well-known/assetlinks.json にこのファイルを設置します。

Intent 起動

準備が終わってやっと Compose の出番です。まあ一瞬ですが。

・・・やっぱりその前に、Intent の結果が帰ってきた時にそれをハンドリングする関数を作成します。

fun handleAuthorizationResponse(result: Intent?) {
    // 注意: どちらも Nullable
    val response = AuthorizationResponse.fromIntent(result)
    val error = AuthorizationException.fromIntent(result)

    authState.update(response, error)

    if (response != null) {
        // ここで accessToken とか idToken とかが取得できます
        // response.accessToken
    }
}

そして今度こそ Compose 。

@Composable
fun LoginPage() {
    val context = LocalContext.current
    val launcher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.StartActivityForResult()
    ) { result -> handleAuthorizationResponse(result.data) }
    val loginIntent = getAuthorizationIntent(context)
    val onLoginButtonClicked = { launcher.launch(loginIntent) }

    Button(onLoginButtonClicked) {
        Text("Login")
    }
}

これで出来上がり!あとはなんかちょうど良い感じに調理してください。

おしまい。

Discussion

ログインするとコメントできます