commit_bridge/trigger/
auth.rs1use async_trait::async_trait;
4use jsonwebtoken::{Algorithm, EncodingKey, Header, encode};
5use reqwest::Client;
6use serde::{Deserialize, Serialize};
7use tracing::info;
8
9use crate::{
10 config::Config,
11 model::Subscription,
12 trigger::error::{AuthError, RequestError},
13};
14
15#[async_trait]
17pub trait Authenticator {
18 async fn request_installation_token(&self, sub: &Subscription) -> Result<String, AuthError>;
20}
21
22pub struct GitHubAuthenticator {
24 pub http_client: reqwest::Client,
26
27 pub config: Config,
29
30 pub encoding_key: EncodingKey,
32}
33
34#[async_trait]
35impl Authenticator for GitHubAuthenticator {
36 async fn request_installation_token(
37 &self,
38 subscription: &Subscription,
39 ) -> Result<String, AuthError> {
40 let jwt = generate_gh_jwt(&self.encoding_key, &self.config)?;
41 request_iat(&self.http_client, &jwt, subscription, &self.config).await
42 }
43}
44
45#[derive(Debug, Serialize, Deserialize)]
52struct GitHubClaims {
53 iat: u64,
55
56 exp: u64,
58
59 iss: String,
61}
62
63pub(super) fn generate_gh_jwt(key: &EncodingKey, config: &Config) -> Result<String, AuthError> {
70 let now = std::time::SystemTime::now()
71 .duration_since(std::time::UNIX_EPOCH)?
72 .as_secs();
73 let claims = GitHubClaims {
74 iat: now - config.auth.clock_drift_buffer.as_secs(),
75 exp: now + config.auth.token_validity.as_secs(),
76 iss: config.auth.client_id.to_string(),
77 };
78
79 let header = Header::new(Algorithm::RS256);
80
81 let jwt = encode(&header, &claims, key)?;
82 Ok(jwt)
83}
84
85pub(super) async fn request_iat(
93 http_client: &Client,
94 jwt: &str,
95 sub: &Subscription,
96 config: &Config,
97) -> Result<String, AuthError> {
98 #[derive(serde::Deserialize)]
99 struct IatResponse {
100 token: String,
101 }
102
103 let api_url = format!(
104 "{}/app/installations/{}/access_tokens",
105 config.github_api.base_url.as_str().trim_end_matches('/'),
106 sub.gh_app_installation_id
107 );
108 let response = http_client
109 .post(&api_url)
110 .bearer_auth(jwt)
111 .header("Accept", config.github_api.accept_header.to_string())
112 .header(
113 "X-GitHub-Api-Version",
114 config.github_api.version.to_string(),
115 )
116 .send()
117 .await?;
118
119 if response.status().is_success() {
120 let response_json = response.json::<IatResponse>().await?;
121 info!("IAT received for subscription {}", sub.target_repo);
122 Ok(response_json.token)
123 } else {
124 Err(AuthError::Server(RequestError::Response {
125 status: response.status(),
126 text: response.text().await?,
127 }))
128 }
129}