1use async_trait::async_trait;
9use chrono::Duration;
10use std::sync::Arc;
11
12use super::{
13 AuthenticationProvider, GitHubApiClient, Installation, InstallationId, InstallationToken,
14 JsonWebToken, JwtClaims, JwtSigner, Repository, SecretProvider, TokenCache,
15};
16use crate::error::AuthError;
17
18#[derive(Debug, Clone)]
20pub struct AuthConfig {
21 pub jwt_expiration: Duration,
23
24 pub jwt_refresh_margin: Duration,
26
27 pub token_cache_ttl: Duration,
29
30 pub token_refresh_margin: Duration,
32
33 pub github_api_url: String,
35
36 pub user_agent: String,
38}
39
40impl Default for AuthConfig {
41 fn default() -> Self {
42 Self {
43 jwt_expiration: Duration::minutes(10),
44 jwt_refresh_margin: Duration::minutes(2),
45 token_cache_ttl: Duration::minutes(55),
46 token_refresh_margin: Duration::minutes(5),
47 github_api_url: "https://api.github.com".to_string(),
48 user_agent: "github-bot-sdk".to_string(),
49 }
50 }
51}
52
53pub struct GitHubAppAuth<S, J, A, C>
58where
59 S: SecretProvider,
60 J: JwtSigner,
61 A: GitHubApiClient,
62 C: TokenCache,
63{
64 secret_provider: Arc<S>,
65 jwt_signer: Arc<J>,
66 api_client: Arc<A>,
67 token_cache: Arc<C>,
68 config: AuthConfig,
69}
70
71impl<S, J, A, C> GitHubAppAuth<S, J, A, C>
72where
73 S: SecretProvider,
74 J: JwtSigner,
75 A: GitHubApiClient,
76 C: TokenCache,
77{
78 pub fn new(
80 secret_provider: S,
81 jwt_signer: J,
82 api_client: A,
83 token_cache: C,
84 config: AuthConfig,
85 ) -> Self {
86 Self {
87 secret_provider: Arc::new(secret_provider),
88 jwt_signer: Arc::new(jwt_signer),
89 api_client: Arc::new(api_client),
90 token_cache: Arc::new(token_cache),
91 config,
92 }
93 }
94
95 pub fn config(&self) -> &AuthConfig {
97 &self.config
98 }
99}
100
101#[async_trait]
102impl<S, J, A, C> AuthenticationProvider for GitHubAppAuth<S, J, A, C>
103where
104 S: SecretProvider + 'static,
105 J: JwtSigner + 'static,
106 A: GitHubApiClient + 'static,
107 C: TokenCache + 'static,
108{
109 async fn app_token(&self) -> Result<JsonWebToken, AuthError> {
110 let app_id = self
112 .secret_provider
113 .get_app_id()
114 .await
115 .map_err(AuthError::SecretError)?;
116
117 let cached_jwt = match self.token_cache.get_jwt(app_id).await {
119 Ok(Some(jwt)) => Some(jwt),
120 Ok(None) => None,
121 Err(_) => {
122 None
125 }
126 };
127
128 if let Some(jwt) = cached_jwt {
129 if !jwt.expires_soon(self.config.jwt_refresh_margin) {
131 return Ok(jwt);
132 }
133 }
134
135 let private_key = self
137 .secret_provider
138 .get_private_key()
139 .await
140 .map_err(AuthError::SecretError)?;
141
142 let now = chrono::Utc::now();
143 let expiration = now + self.config.jwt_expiration;
144
145 let claims = JwtClaims {
146 iss: app_id,
147 iat: now.timestamp(),
148 exp: expiration.timestamp(),
149 };
150
151 let jwt = self
152 .jwt_signer
153 .sign_jwt(claims, &private_key)
154 .await
155 .map_err(AuthError::SigningError)?;
156
157 let _ = self.token_cache.store_jwt(jwt.clone()).await;
159
160 Ok(jwt)
161 }
162
163 async fn installation_token(
164 &self,
165 installation_id: InstallationId,
166 ) -> Result<InstallationToken, AuthError> {
167 let cached_token = match self
169 .token_cache
170 .get_installation_token(installation_id)
171 .await
172 {
173 Ok(Some(token)) => Some(token),
174 Ok(None) => None,
175 Err(_) => {
176 None
179 }
180 };
181
182 if let Some(token) = cached_token {
183 if !token.expires_soon(self.config.token_refresh_margin) {
185 return Ok(token);
186 }
187 }
188
189 let jwt = self.app_token().await?;
191
192 let token = self
193 .api_client
194 .create_installation_access_token(installation_id, &jwt)
195 .await
196 .map_err(AuthError::ApiError)?;
197
198 let _ = self
200 .token_cache
201 .store_installation_token(token.clone())
202 .await;
203
204 Ok(token)
205 }
206
207 async fn refresh_installation_token(
208 &self,
209 installation_id: InstallationId,
210 ) -> Result<InstallationToken, AuthError> {
211 let _ = self
213 .token_cache
214 .invalidate_installation_token(installation_id)
215 .await;
216
217 let jwt = self.app_token().await?;
219
220 let token = self
221 .api_client
222 .create_installation_access_token(installation_id, &jwt)
223 .await
224 .map_err(AuthError::ApiError)?;
225
226 let _ = self
228 .token_cache
229 .store_installation_token(token.clone())
230 .await;
231
232 Ok(token)
233 }
234
235 async fn list_installations(&self) -> Result<Vec<Installation>, AuthError> {
236 let jwt = self.app_token().await?;
237
238 self.api_client
239 .list_app_installations(&jwt)
240 .await
241 .map_err(AuthError::ApiError)
242 }
243
244 async fn get_installation_repositories(
245 &self,
246 installation_id: InstallationId,
247 ) -> Result<Vec<Repository>, AuthError> {
248 let token = self.installation_token(installation_id).await?;
249
250 self.api_client
251 .list_installation_repositories(installation_id, &token)
252 .await
253 .map_err(AuthError::ApiError)
254 }
255}
256
257#[cfg(test)]
258#[path = "tokens_tests.rs"]
259mod tests;