Skip to main content

commit_bridge/trigger/
auth.rs

1//! Authentication and authorization to request services.
2
3use 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/// Provides authentication functionality.
16#[async_trait]
17pub trait Authenticator {
18    /// Requests a GitHub Installation Access Token.
19    async fn request_installation_token(&self, sub: &Subscription) -> Result<String, AuthError>;
20}
21
22/// An [`Authenticator`] for GitHub.
23pub struct GitHubAuthenticator {
24    /// The HTTP client to make requests to the authentication server.
25    pub http_client: reqwest::Client,
26
27    /// Application configuration.
28    pub config: Config,
29
30    /// The key to sign JWTs.
31    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/// Payload that GitHub expects in the JWT.
46///
47/// Read more on [GitHub's documentation][jwt_docs].
48///
49/// <!-- LINKS -->
50/// [jwt_docs]: https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-json-web-token-jwt-for-a-github-app
51#[derive(Debug, Serialize, Deserialize)]
52struct GitHubClaims {
53    /// Issued at time (UNIX time).
54    iat: u64,
55
56    /// Expiration time (UNIX time).
57    exp: u64,
58
59    /// Issuer: GitHub App's Client ID.
60    iss: String,
61}
62
63/// Generates a JWT to authenticate access to the GitHub App.
64///
65/// Implementation based on [GitHub's documentation][jwt_docs].
66///
67/// <!-- LINKS -->
68/// [jwt_docs]: https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-json-web-token-jwt-for-a-github-app
69pub(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
85/// Requests an Installation Access Token (IAT)
86/// to operate on a GitHub App installation.
87///
88/// Implementation based on [GitHub's documentation][iat_docs].
89///
90/// <!-- LINKS -->
91/// [iat_docs]: https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app
92pub(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}