アプリ開発備忘録

PlayStationMobile、Android、UWPの開発備忘録

【Google Cloud】サービスアカウントでAPIを叩く【Firebase】

FirebaseのAPIをサービスアカウントを使って、Googleのライブラリ無しで呼び出します。

参考URL: https://cloud.google.com/functions/docs/securing/authenticating#exchanging_a_self-signed_jwt_for_a_google-signed_id_token

サービスアカウントを作る

https://console.cloud.google.com/iam-admin/serviceaccounts

今回はFirebase App DistributionのAPIを使用したかった為、こちらの権限を与えました。

鍵を作る

推奨のJsonで作ります。以下のようなjsonがダウンロードできます

{
  "type": "service_account",
  "project_id": "api-0000000000000000000-000000",
  "private_key_id": "2222222222222222222222222222222222222222",
  "private_key": "-----BEGIN PRIVATE KEY-----\nHOGE\nHOGE=\n-----END PRIVATE KEY-----\n",
  "client_email": "test-app@api-0000000000000000000-000000.iam.gserviceaccount.com",
  "client_id": "111111111111111111111",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/test-app%40api-0000000000000000000-000000.iam.gserviceaccount.com"
}

リクエストを行う

流れ

以下のような流れで作成していきます。

  • jwtを作る
  • jwtを使ってTokenを取得する
  • Tokenを使用してリクエストを行う

今回はKotlin+JDK17で書きます。

jwtからTokenを取得する

使用ライブラリ

jwtライブラリとjsonシリアライズライブラリだけ追加で使用します。

build.gradle.kts

plugins {
    kotlin("jvm") version "1.7.10"
    kotlin("plugin.serialization") version "1.7.10"
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("com.auth0:java-jwt:3.19.2")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.0-RC")
}

コード

まずはサービスアカウントの private_key のキー部分だけを取り出して、auth0が求める型に変換します。

import

import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import org.intellij.lang.annotations.Language
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.security.KeyFactory
import java.security.interfaces.RSAPrivateKey
import java.security.spec.PKCS8EncodedKeySpec
import java.util.*

/**
 * @param privateKeyString private_key の値
 * @param issuer client_email の値
 **/
fun get(
    privateKeyString: String,
    issuer: String,
): String {
    val privateKey: RSAPrivateKey = privateKeyString
        .replace("-----BEGIN PRIVATE KEY-----", "")
        .replace("-----END PRIVATE KEY-----", "")
        .replace("\n", "")
        .let { Base64.getDecoder().decode(it) }
        .let { PKCS8EncodedKeySpec(it) }
        .let {
            KeyFactory
                .getInstance("RSA")
                .generatePrivate(it)
                    as RSAPrivateKey
        }
    // ...
}

期限は1時間に設定します。
更にClaimで scope を設定します。

自分が使用したいAPIは以下なので、ドキュメントにある通り、 https://www.googleapis.com/auth/cloud-platform を設定します。
https://firebase.google.com/docs/reference/app-distribution/rest/v1/projects.apps.releases/list

// requestUrlは `token_uri` の値
val requestUrl = "https://oauth2.googleapis.com/token"
val time = Calendar.getInstance()
val jwt = JWT.create()
    .withIssuer(issuer)
    .withAudience(requestUrl)
    .withIssuedAt(time.time)
    .withExpiresAt(time.also { it.add(Calendar.HOUR, 1) }.time)
    .withClaim(
        "scope", "https://www.googleapis.com/auth/cloud-platform"
    )
    .sign(Algorithm.RSA256(null, privateKey))

POSTリクエストを投げます。
grant_typeには urn:ietf:params:oauth:grant-type:jwt-bearer , assertionには先程生成したjwtを設定します。

val httpRequestResult = HttpClient.newHttpClient()
    .send(
        HttpRequest.newBuilder()
            .uri(URI(requestUrl))
            .header("Content-Type", "application/json")
            .POST(
                HttpRequest.BodyPublishers.ofString(
                    run {
                        @Language("json")
                        val json = """
                        {
                            "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
                            "assertion": "$jwt"
                        }
                    """.trimIndent()
                        json
                    }
                )
            )
            .build(),
        HttpResponse.BodyHandlers.ofString()
    )

以下のような構造のレスポンスが得られます。

@Serializable
private data class TokenRequestResult(
    @SerialName("access_token") val accessToken: String,
    @SerialName("expires_in") val expiresIn: Int,
    @SerialName("token_type") val tokenType: String,
)

結果をパースします。これでTokenが得られます。

val requestResult = Json.Default.decodeFromString<TokenRequestResult>(body)
println(requestResult)

return requestResult.accessToken

TokenでAPIを叩く

ここまでくればAPIを叩くだけです。
Authorization ヘッダに先程のjwtを設定します。

val requestUrl =
    "https://firebaseappdistribution.googleapis.com/v1/projects/000000000000/apps/1:000000000000:android:9999999999999999/releases"

val result = HttpClient.newHttpClient()
    .send(
        HttpRequest.newBuilder()
            .uri(URI(requestUrl))
            .header("Content-Type", "application/json")
            .header("Authorization", "Bearer $token")
            .GET()
            .build(),
        HttpResponse.BodyHandlers.ofString()
    )

println(result.body())