use async_trait::async_trait;
use jsonwebtoken::{Algorithm, EncodingKey, Header, encode};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use tracing::info;
use crate::{
config::Config,
model::Subscription,
trigger::error::{AuthError, RequestError},
};
#[async_trait]
pub trait Authenticator {
async fn request_installation_token(&self, sub: &Subscription) -> Result<String, AuthError>;
}
pub struct GitHubAuthenticator {
pub http_client: reqwest::Client,
pub config: Config,
pub encoding_key: EncodingKey,
}
#[async_trait]
impl Authenticator for GitHubAuthenticator {
async fn request_installation_token(
&self,
subscription: &Subscription,
) -> Result<String, AuthError> {
let jwt = generate_gh_jwt(&self.encoding_key, &self.config)?;
request_iat(&self.http_client, &jwt, subscription, &self.config).await
}
}
#[derive(Debug, Serialize, Deserialize)]
struct GitHubClaims {
iat: u64,
exp: u64,
iss: String,
}
pub(super) fn generate_gh_jwt(key: &EncodingKey, config: &Config) -> Result<String, AuthError> {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)?
.as_secs();
let claims = GitHubClaims {
iat: now - config.auth.clock_drift_buffer.as_secs(),
exp: now + config.auth.token_validity.as_secs(),
iss: config.auth.client_id.to_string(),
};
let header = Header::new(Algorithm::RS256);
let jwt = encode(&header, &claims, key)?;
Ok(jwt)
}
pub(super) async fn request_iat(
http_client: &Client,
jwt: &str,
sub: &Subscription,
config: &Config,
) -> Result<String, AuthError> {
#[derive(serde::Deserialize)]
struct IatResponse {
token: String,
}
let api_url = format!(
"{}/app/installations/{}/access_tokens",
config.github_api.base_url.as_str().trim_end_matches('/'),
sub.gh_app_installation_id
);
let response = http_client
.post(&api_url)
.bearer_auth(jwt)
.header("Accept", config.github_api.accept_header.to_string())
.header(
"X-GitHub-Api-Version",
config.github_api.version.to_string(),
)
.send()
.await?;
if response.status().is_success() {
let response_json = response.json::<IatResponse>().await?;
info!("IAT received for subscription {}", sub.target_repo);
Ok(response_json.token)
} else {
Err(AuthError::Server(RequestError::Response {
status: response.status(),
text: response.text().await?,
}))
}
}