なにかと社内のデータが入ってるといえば Google スプレッドシートですが、社内ツールを Rust で作ってみようとした際、スプシ操作するクレートの google_sheets4 というのだと、 Google サービスアカウントでの認証には対応していなかったので、直接アクセストークンを取って REST API を叩く形で実装しました。
アクセストークンを取り方がわかれば他の G Suite や Map や GCP や Firebase などの API も操作できるので、その部分のメモを残しておきます。
認証の仕様は Using OAuth 2.0 for Server to Server Applications に書いてあって、流れは Hosting REST API を使用してサイトにデプロイするがわかりやすくて、よくわからないところは Google API クライアント ライブラリの実装をみてやりました。
JWT を作る
serde_json を使って環境変数に入ってるサービスアカウントの 秘密鍵 JSON をパースします。
[dependencies]
serde = { version = "^1", features = ["derive"] }
serde_json = "^1"
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 を使ってJWTの発行時刻と有効期限のタイムスタンプを作ります。
[dependencies]
chrono = "^0.4"
use chrono::{Duration, Utc};
fn main() {
let now = Utc::now();
let iat = now.timestamp();
let exp = (now + Duration::minutes(60)).timestamp();
jsonwebtoken を使って JWT を作成します。ヘッダ、クレームセットの仕様はこちらに記述されています。
スコープに何を指定するかは、使いたい API のドキュメントに書いてあります。 (例)
[dependencies]
serde = { version = "^1", features = ["derive"] }
jsonwebtoken = "^7"
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 を使ってトークンリクエストを投げます。リクエストの仕様は上記同様こちらに記述されています。
AWS Lambda などシステムネイティブ TLS が使えない環境で動かすことを想定しています。
[dependencies]
serde_json = "^1"
[dependencies.reqwest]
version = "^0.10"
default-features = false
features = ["blocking", "json", "rustls-tls"]
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 を構造体にしないパターンでパースしてアクセストークンを取り出します。
[dependencies]
serde_json = "^1"
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 にあるものは大体いけるんじゃないですかね。
(サンプル) スプシのシートの値を取得する
このようなシートを用意して、
値を取得する場合は Method: spreadsheets.values.get を叩きます。
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 よく分かってないので、ご指摘・アドバイス等あればよろしくお願いします🙇