1use std::convert::TryFrom;
2use std::fmt::Debug;
3use std::ops::Add;
4use std::path::PathBuf;
5
6use async_trait::async_trait;
7use jiff::{SignedDuration, Timestamp};
8use secret_vault_value::SecretValue;
9
10pub mod auth_token_generator;
11pub mod credentials;
12pub mod metadata;
13
14pub use credentials::{from_env_var, from_well_known_file};
15use metadata::from_metadata;
16
17pub use credentials::{from_file, from_json};
18use tracing::*;
19
20mod ext_creds_source;
21mod gce;
22
23pub type BoxSource = Box<dyn Source + Send + Sync + 'static>;
24
25#[async_trait]
26pub trait Source {
27 async fn token(&self) -> crate::error::Result<Token>;
28}
29
30pub async fn create_source(
31 token_source_type: TokenSourceType,
32 token_scopes: Vec<String>,
33) -> crate::error::Result<BoxSource> {
34 match token_source_type {
35 TokenSourceType::Default => Ok(find_default(&token_scopes).await?),
36 TokenSourceType::Json(json) => Ok(from_json(json.as_bytes(), &token_scopes)?.into()),
37 TokenSourceType::File(path) => Ok(from_file(path, &token_scopes)?.into()),
38 TokenSourceType::MetadataServer => {
39 if let Some(src) = from_metadata(&token_scopes, "default".to_string()).await? {
40 Ok(src.into())
41 } else {
42 Err(crate::error::ErrorKind::TokenSource.into())
43 }
44 }
45 TokenSourceType::MetadataServerWithAccount(account) => {
46 if let Some(src) = from_metadata(&token_scopes, account).await? {
47 Ok(src.into())
48 } else {
49 Err(crate::error::ErrorKind::TokenSource.into())
50 }
51 }
52 TokenSourceType::ExternalSource(token_source) => Ok(token_source),
53 }
54}
55
56pub async fn find_default(token_scopes: &[String]) -> crate::error::Result<BoxSource> {
61 debug!("Finding default token for scopes: {:?}", token_scopes);
62
63 if let Some(src) = from_env_var(token_scopes)? {
64 debug!("Creating token based on environment variable: GOOGLE_APPLICATION_CREDENTIALS");
65 return Ok(src.into());
66 }
67 if let Some(src) = from_well_known_file(token_scopes)? {
68 debug!("Creating token based on standard config files such as application_default_credentials.json");
69 return Ok(src.into());
70 }
71 if let Some(src) = from_metadata(token_scopes, "default".to_string()).await? {
72 debug!("Creating token based on metadata server");
73 return Ok(src.into());
74 }
75 warn!("None of the possible sources detected for Google OAuth token");
76 Err(crate::error::ErrorKind::TokenSource.into())
77}
78
79#[derive(Debug, Clone)]
80pub struct Token {
81 pub token_type: String,
82 pub token: SecretValue,
83 pub expiry: Timestamp,
84}
85
86impl Token {
87 pub fn new(token_type: String, token: SecretValue, expiry: Timestamp) -> Self {
88 Self {
89 token_type,
90 token,
91 expiry,
92 }
93 }
94 pub fn header_value(&self) -> String {
95 format!("{} {}", self.token_type, self.token.as_sensitive_str())
96 }
97
98 pub async fn generate_for_scopes(
99 token_source_type: TokenSourceType,
100 token_scopes: Vec<String>,
101 ) -> crate::error::Result<Token> {
102 let token_source: BoxSource = create_source(token_source_type, token_scopes).await?;
103 token_source.token().await
104 }
105}
106
107impl TryFrom<TokenResponse> for Token {
108 type Error = crate::error::Error;
109
110 fn try_from(v: TokenResponse) -> Result<Self, Self::Error> {
111 if v.token_type.is_empty()
112 || v.access_token.as_sensitive_bytes().is_empty()
113 || v.expires_in == 0
114 {
115 Err(crate::error::ErrorKind::TokenData.into())
116 } else {
117 Ok(Token {
118 token_type: v.token_type,
119 token: v.access_token,
120 expiry: Timestamp::now()
121 .add(SignedDuration::from_secs(v.expires_in.try_into().unwrap())),
122 })
123 }
124 }
125}
126
127#[derive(Debug, serde::Deserialize)]
128struct TokenResponse {
129 token_type: String,
130 access_token: SecretValue,
131 expires_in: u64,
132}
133
134impl TryFrom<&str> for TokenResponse {
135 type Error = crate::error::Error;
136
137 fn try_from(v: &str) -> Result<Self, Self::Error> {
138 let resp = serde_json::from_str(v).map_err(crate::error::ErrorKind::TokenJson)?;
139 Ok(resp)
140 }
141}
142
143#[derive(Debug, Clone)]
144pub struct ExternalJwtFunctionSource<F, FN>
145where
146 F: std::future::Future<Output = crate::error::Result<Token>> + Send + Sync + 'static,
147 FN: Fn() -> F + Send + Sync,
148{
149 token_fn: FN,
150}
151
152impl<F, FN> ExternalJwtFunctionSource<F, FN>
153where
154 F: std::future::Future<Output = crate::error::Result<Token>> + Send + Sync + 'static,
155 FN: Fn() -> F + Send + Sync,
156{
157 pub fn new(token_fn: FN) -> Self {
158 Self { token_fn }
159 }
160}
161
162#[async_trait]
163impl<F, FN> Source for ExternalJwtFunctionSource<F, FN>
164where
165 F: std::future::Future<Output = crate::error::Result<Token>> + Send + Sync,
166 FN: Fn() -> F + Send + Sync,
167{
168 async fn token(&self) -> crate::error::Result<Token> {
169 (self.token_fn)().await
170 }
171}
172
173pub enum TokenSourceType {
174 Default,
175 Json(String),
176 File(PathBuf),
177 MetadataServer,
178 MetadataServerWithAccount(String),
179 ExternalSource(BoxSource),
180}
181
182impl Debug for TokenSourceType {
183 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184 match self {
185 TokenSourceType::Default => write!(f, "Default"),
186 TokenSourceType::Json(_) => write!(f, "Json"),
187 TokenSourceType::File(_) => write!(f, "File"),
188 TokenSourceType::MetadataServer => write!(f, "MetadataServer"),
189 TokenSourceType::MetadataServerWithAccount(_) => write!(f, "MetadataServerWithAccount"),
190 TokenSourceType::ExternalSource(_) => write!(f, "ExternalSource"),
191 }
192 }
193}
194
195#[cfg(test)]
196mod test {
197 use super::*;
198
199 macro_rules! test_token_try_from {
200 () => {};
201 ($name:ident, $in:expr, $ok:expr; $($tt:tt)*) => {
202 #[test]
203 fn $name() {
204 assert_eq!(Token::try_from($in).is_ok(), $ok)
205 }
206 test_token_try_from!($($tt)*);
207 };
208 }
209
210 test_token_try_from!(
211 test_token_try_from_token_type,
212 TokenResponse {
213 token_type: String::new(),
214 access_token: "secret".into(),
215 expires_in: 1,
216 },
217 false;
218
219 test_token_try_from_access_token,
220 TokenResponse {
221 token_type: "type".into(),
222 access_token: "".into(),
223 expires_in: 1,
224 },
225 false;
226
227 test_token_try_from_expires_in,
228 TokenResponse {
229 token_type: "type".into(),
230 access_token: "secret".into(),
231 expires_in: 0,
232 },
233 false;
234
235 test_token_try_from_ok,
236 TokenResponse {
237 token_type: "type".into(),
238 access_token: "secret".into(),
239 expires_in: 1,
240 },
241 true;
242 );
243}