use crate::token::{Token, TokenValue};
use anyhow::Result;
use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::{borrow::Borrow, collections::HashMap};
use tokio::{fs::OpenOptions, io::AsyncReadExt};
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(untagged)]
enum CredentialType {
#[allow(non_camel_case_types)]
AuthorizedUser(ApplicationDefaultCredentials),
#[allow(non_camel_case_types)]
ServiceAccount(ServiceAccountCredentials),
MetadataServer,
}
#[derive(Debug, Clone)]
pub(crate) struct Credentials {
scopes: String,
credential_type: CredentialType,
}
impl Credentials {
pub(crate) async fn new(scopes: &[&str]) -> Result<Self> {
let scopes = scopes.join(" ");
let credential_type = if let Ok(gac) = Self::try_gac().await {
gac
} else if let Ok(adc) = Self::try_well_known().await {
adc
} else {
CredentialType::MetadataServer
};
Ok(Self {
scopes,
credential_type,
})
}
async fn try_gac() -> Result<CredentialType> {
let gac = std::env::var("GOOGLE_APPLICATION_CREDENTIALS")?;
let path = PathBuf::from(gac);
Self::from_file(path).await
}
async fn try_well_known() -> Result<CredentialType> {
let home = std::env::var("HOME")?;
let config_path = ".config/gcloud/application_default_credentials.json";
let home_path = PathBuf::from(home).join(config_path);
Self::from_file(home_path).await
}
async fn from_file(path: PathBuf) -> Result<CredentialType> {
let mut file = OpenOptions::new().read(true).open(path).await?;
let mut buf = vec![];
file.read_to_end(&mut buf).await?;
let credentials: CredentialType = serde_json::from_slice(&buf)?;
Ok(credentials)
}
pub(crate) async fn token(&self) -> Result<Token> {
let token = match self.credential_type.borrow() {
CredentialType::AuthorizedUser(adc) => adc.token().await?,
CredentialType::ServiceAccount(sa) => sa.token(&self.scopes).await?,
CredentialType::MetadataServer => Self::from_metadata_server().await?,
};
Ok(token)
}
async fn from_metadata_server() -> Result<Token> {
let url = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token";
let client = reqwest::Client::new();
let token_value: TokenValue = client
.get(url)
.header("Metadata-Flavor", "Google")
.send()
.await?
.json()
.await?;
Ok(Token::new(token_value))
}
}
#[derive(Deserialize, Serialize, Debug, Clone)]
struct ApplicationDefaultCredentials {
client_id: String,
client_secret: String,
refresh_token: String,
r#type: String,
}
impl ApplicationDefaultCredentials {
async fn token(&self) -> Result<Token> {
let client = reqwest::Client::new();
let mut form: HashMap<String, String> = HashMap::new();
form.insert(String::from("client_id"), self.client_id.clone());
form.insert(String::from("client_secret"), self.client_secret.clone());
form.insert(String::from("refresh_token"), self.refresh_token.clone());
form.insert(String::from("grant_type"), String::from("refresh_token"));
let resp = client
.post("https://oauth2.googleapis.com/token")
.form(&form)
.send()
.await?;
let resp_string = resp.text().await?;
let token_value = serde_json::from_str(&resp_string)?;
Ok(Token::new(token_value))
}
}
#[derive(Deserialize, Serialize, Debug, Clone)]
struct ServiceAccountCredentials {
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,
}
#[derive(Deserialize, Serialize, Debug)]
struct Claims {
iss: String,
scope: String,
aud: String,
exp: u64,
iat: u64,
}
impl ServiceAccountCredentials {
async fn token(&self, scope: &str) -> Result<Token> {
let ts = std::time::SystemTime::now()
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
let claims = Claims {
iss: self.client_email.clone(),
scope: scope.to_string(),
aud: self.token_uri.clone(),
iat: ts,
exp: ts + 3600,
};
let header = Header {
typ: Some("JWT".to_owned()),
alg: Algorithm::RS256,
cty: None,
jku: None,
kid: None,
x5u: None,
x5t: None,
};
let key = EncodingKey::from_rsa_pem(self.private_key.as_ref()).unwrap();
let jwt = encode(&header, &claims, &key)?;
let mut form: HashMap<&str, &str> = HashMap::new();
form.insert("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer");
form.insert("assertion", &jwt);
let client = reqwest::Client::new();
let resp = client.post(&self.token_uri).form(&form).send().await?;
let token_value = resp.json::<TokenValue>().await?;
Ok(Token::new(token_value))
}
}