Skip to content

Instantly share code, notes, and snippets.

@algon-320
Last active September 6, 2021 04:05
Show Gist options
  • Save algon-320/30d20e638b4d59acf04bf1b05faee643 to your computer and use it in GitHub Desktop.
Save algon-320/30d20e638b4d59acf04bf1b05faee643 to your computer and use it in GitHub Desktop.
Get a OAuth 2.0 access token in Rust (Google APIs)
// [dependencies]
// jwt = { version = "0.14.0", features = ["openssl"] }
// openssl = "0.10.32"
// serde_json = "1"
// Docs:
// https://developers.google.com/identity/protocols/oauth2/service-account
use openssl::pkey::{PKey, Private};
fn get_private_key<P>(json_filename: P) -> PKey<Private>
where
P: AsRef<std::path::Path>,
{
let json_bytes = std::fs::read(json_filename).expect("cannot open the JSON");
let json: serde_json::Value = serde_json::from_slice(&json_bytes).expect("invalid JSON");
let pem = json
.get("private_key")
.expect("invalid credential JSON")
.as_str()
.unwrap();
PKey::private_key_from_pem(pem.as_bytes()).unwrap()
}
fn create_jwt(service_account_email: &str, scopes: &[&str], key: PKey<Private>) -> String {
let header = jwt::Header {
algorithm: jwt::AlgorithmType::Rs256,
type_: Some(jwt::header::HeaderType::JsonWebToken),
..Default::default()
};
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let claims = jwt::claims::Claims {
registered: jwt::claims::RegisteredClaims {
issuer: Some(service_account_email.to_owned()),
audience: Some("https://www.googleapis.com/oauth2/v4/token".to_owned()),
expiration: Some(now + 3600), // 1-hour
issued_at: Some(now),
..Default::default()
},
private: {
let mut claims = std::collections::BTreeMap::new();
claims.insert("scope".into(), scopes.join(" ").into());
claims
},
};
let digest = openssl::hash::MessageDigest::sha256();
let signing_key = jwt::algorithm::openssl::PKeyWithDigest { digest, key };
use jwt::SignWithKey as _;
let token = jwt::Token::new(header, claims)
.sign_with_key(&signing_key)
.unwrap();
token.as_str().to_owned()
}
fn main() {
let key = get_private_key("./credential.json");
let service_account_email = "xxxxxxxx@yyyyyyyy.iam.gserviceaccount.com";
let scope = ["https://www.googleapis.com/auth/firebase.messaging"];
let token = create_jwt(service_account_email, &scope, key);
println!(
"curl -H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer' \
-d 'assertion={}' \
https://www.googleapis.com/oauth2/v4/token",
token
);
}
// [dependencies]
// jwt = { version = "0.14.0", features = ["openssl"] }
// openssl = "0.10.32"
// serde_json = "1"
// reqwest = { version = "0.11.4", features = ["blocking", "json"] }
// Ref: https://developers.google.com/identity/protocols/oauth2/service-account
use openssl::pkey::{PKey, Private};
fn get_private_key<P>(json_filename: P) -> PKey<Private>
where
P: AsRef<std::path::Path>,
{
let json_bytes = std::fs::read(json_filename).expect("cannot open the JSON");
let json: serde_json::Value = serde_json::from_slice(&json_bytes).expect("invalid JSON");
let pem = json
.get("private_key")
.expect("invalid credential JSON")
.as_str()
.unwrap();
PKey::private_key_from_pem(pem.as_bytes()).unwrap()
}
fn create_jwt(service_account_email: &str, scopes: &[&str], key: PKey<Private>) -> String {
let header = jwt::Header {
algorithm: jwt::AlgorithmType::Rs256,
type_: Some(jwt::header::HeaderType::JsonWebToken),
..Default::default()
};
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let claims = jwt::claims::Claims {
registered: jwt::claims::RegisteredClaims {
issuer: Some(service_account_email.to_owned()),
audience: Some("https://www.googleapis.com/oauth2/v4/token".to_owned()),
expiration: Some(now + 3600), // 1-hour
issued_at: Some(now),
..Default::default()
},
private: {
let mut claims = std::collections::BTreeMap::new();
claims.insert("scope".into(), scopes.join(" ").into());
claims
},
};
let digest = openssl::hash::MessageDigest::sha256();
let signing_key = jwt::algorithm::openssl::PKeyWithDigest { digest, key };
use jwt::SignWithKey as _;
let token = jwt::Token::new(header, claims)
.sign_with_key(&signing_key)
.unwrap();
token.as_str().to_owned()
}
fn push(project_id: &str, access_token: &str, title: &str, text: &str, destination_token: &str) {
let client = reqwest::blocking::Client::new();
let req = serde_json::json!({
"message": {
"data": { "title": title, "text": text },
"token": destination_token,
}
});
let resp = client
.post(format!(
"https://fcm.googleapis.com/v1/projects/{}/messages:send",
project_id
))
.bearer_auth(&access_token)
.json(&req)
.send()
.expect("Failed to push the message");
if resp.status().is_success() {
println!("successfully pushed!");
} else {
println!("{}: {:?}", resp.status(), resp.text());
}
}
fn main() {
let project_id = "yyyyyyyy";
let service_account_email = "xxxxxxxx@yyyyyyyy.iam.gserviceaccount.com";
let device_token = "zzzzzzzz";
let key = get_private_key("./credential.json");
let scope = ["https://www.googleapis.com/auth/firebase.messaging"];
let jwt = create_jwt(service_account_email, &scope, key);
// println!(
// "curl -H 'Content-Type: application/x-www-form-urlencoded' \
// -d 'grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer' \
// -d 'assertion={}' \
// https://www.googleapis.com/oauth2/v4/token",
// jwt
// );
let client = reqwest::blocking::Client::new();
let response = client
.post("https://www.googleapis.com/oauth2/v4/token")
.form(&[
("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"),
("assertion", jwt.as_str()),
])
.send()
.expect("Failed to get access token");
let access_token = {
let response_json: serde_json::Value = response.json().unwrap();
response_json
.get("access_token")
.unwrap()
.as_str()
.unwrap()
.to_owned()
};
push(
project_id,
&access_token,
"Hello, World", // data.title
"This is a sample notification.", // data.text
device_token,
);
// Android side (Kotlin):
// override fun onMessageReceived(message: RemoteMessage) {
// message.data?.let { data ->
// val title = data["title"] ?: "(empty)"
// val text = data["text"] ?: "(empty)"
// // doSomethingCool(title, text)
// }
// }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment