Rust で Google サービスアカウントの認証 (Sheets からデータ取得サンプル)

なにかと社内のデータが入ってるといえば Google スプレッドシートですが、社内ツールを Rust で作ってみようとした際、スプシ操作するクレートの [google_sheets4](https://docs.rs/google-sheets4/1.0.14+20200630/google_sheets4/) というのだと、 Google サービスアカウントでの認証には対応していなかったので、直接アクセストークンを取って REST API を叩く形で実装しました。

アクセストークンを取り方がわかれば他の G Suite や Map や GCP や Firebase などの API も操作できるので、その部分のメモを残しておきます。

認証の仕様は [Using OAuth 2.0 for Server to Server Applications](https://developers.google.com/identity/protocols/oauth2/service-account) に書いてあって、流れは [Hosting REST API を使用してサイトにデプロイする](https://firebase.google.com/docs/hosting/api-deploy)がわかりやすくて、よくわからないところは [Google API クライアント ライブラリ](https://developers.google.com/api-client-library/)の実装をみてやりました。

## JWT を作る
[serde_json](https://docs.serde.rs/serde_json/) を使って環境変数に入ってるサービスアカウントの 秘密鍵 JSON をパースします。

```toml:Cargo.toml
[dependencies]
serde = { version = "^1", features = ["derive"] }
serde_json = "^1"
```

```rs
use std::env;
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize)]
struct GoogleCredential {
    r#type: String,
    project_id: String,
    private_key_id: String,
    private_key: String,
    client_email: String,
    client_id: String,
    auth_uri: String,
    token_uri: String,
    auth_provider_x509_cert_url: String,
    client_x509_cert_url: String,
}

fn main() {
    let google_credential: GoogleCredential = serde_json::from_str(&env::var("GOOGLE_CREDENTIAL").unwrap()).unwrap();
```

[chrono](https://docs.rs/chrono/) を使ってJWTの発行時刻と有効期限のタイムスタンプを作ります。

```toml:Cargo.toml
[dependencies]
chrono = "^0.4"
```

```rs
use chrono::{Duration, Utc};

fn main() {
    let now = Utc::now();
    let iat = now.timestamp();
    let exp = (now + Duration::minutes(60)).timestamp();
```

[jsonwebtoken](https://docs.rs/jsonwebtoken/) を使って JWT を作成します。ヘッダ、クレームセットの仕様は[こちら](https://developers.google.com/identity/protocols/oauth2/service-account#httprest)に記述されています。
スコープに何を指定するかは、使いたい API のドキュメントに書いてあります。 ([例](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/get#authorization-scopes
)) 

```toml:Cargo.toml
[dependencies]
serde = { version = "^1", features = ["derive"] }
jsonwebtoken = "^7"
```

```rs
use serde::{Deserialize, Serialize};
use jsonwebtoken::{encode, Header, Algorithm, EncodingKey};

#[derive(Debug, Serialize)]
struct Claims {
    iss: String,
    scope: String,
    aud: String,
    exp: i64,
    iat: i64,
}

fn main() {
    let mut header = Header::default();
    header.typ = Some("JWT".to_string());
    header.alg = Algorithm::RS256;

    let my_claims =
        Claims { 
            iss: google_credential.client_email,
            scope: "https://www.googleapis.com/auth/spreadsheets".to_string(),
            aud: google_credential.token_uri,
            exp: exp,
            iat: iat,
        };

    let jwt = encode(&header, &my_claims, &EncodingKey::from_rsa_pem(google_credential.private_key.as_bytes()).unwrap()).unwrap();
```


## アクセストークンを取得する
serde_json と [reqwest](https://docs.rs/reqwest/) を使ってトークンリクエストを投げます。リクエストの仕様は上記同様[こちら](https://developers.google.com/identity/protocols/oauth2/service-account#httprest)に記述されています。
AWS Lambda などシステムネイティブ TLS が使えない環境で動かすことを想定しています。

```toml:Cargo.toml
[dependencies]
serde_json = "^1"

[dependencies.reqwest]
version = "^0.10"
default-features = false
features = ["blocking", "json", "rustls-tls"]
```

```rs
use serde_json::json;
use reqwest::blocking::Client;

fn main() {
    let token_body = json!({
        "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
        "assertion": jwt
    });
    
    let token_response = Client::new()
        .post(&my_claims.aud)
        .json(&token_body)
        .send()
        .unwrap();
```

serde_json を使ってレスポンスボディの JSON を構造体にしないパターンでパースしてアクセストークンを取り出します。

```toml:Cargo.toml
[dependencies]
serde_json = "^1"
```

```rs
use serde_json::Value;

fn main() {
    let token_response_body: Value = token_response.json().unwrap();
    let access_token = token_response_body.get("access_token").unwrap().as_str().unwrap();
```

これでアクセストークンが作成できたので、あとは使いたいサービスの API を叩く感じです。
[API Library](https://console.developers.google.com/apis/library) にあるものは大体いけるんじゃないですかね。

## (サンプル) スプシのシートの値を取得する
このようなシートを用意して、
![Screen Shot 2020-07-26 at 19.48.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142668/c2d886e4-b28c-ee48-ff07-f2b2bd82d9b8.png)
値を取得する場合は [Method: spreadsheets.values.get](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/get) を叩きます。

```rs
use serde_json::Value;
use reqwest::blocking::Client;

fn main() {
    let sheets_response = Client::new()
        .get("https://sheets.googleapis.com/v4/spreadsheets/{your spreadsheet id}/values/A1:C")
        .bearer_auth(access_token)
        .send()
        .unwrap();

    let values = sheets_response.json::<Value>().unwrap().get("values").unwrap());
    // values: [["A1","B1","C1"],["A2","B2","C2"]]
```

という感じでとれました。

以上です。
Rust よく分かってないので、ご指摘・アドバイス等あればよろしくお願いします🙇