tame_oauth/gcp/
metadata_server.rs

1use super::TokenResponse;
2use crate::{
3    error::{self, Error},
4    id_token::{IdTokenOrRequest, IdTokenProvider},
5    token::{RequestReason, Token, TokenOrRequest, TokenProvider},
6    token_cache::CachedTokenProvider,
7    IdToken,
8};
9
10const METADATA_URL: &str =
11    "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts";
12
13/// [Provides tokens](https://cloud.google.com/compute/docs/instances/verifying-instance-identity)
14/// using the metadata server accessible when running from within GCP.
15/// Caches tokens internally.
16pub type MetadataServerProvider = CachedTokenProvider<MetadataServerProviderInner>;
17impl MetadataServerProvider {
18    pub fn new(account_name: Option<String>) -> Self {
19        CachedTokenProvider::wrap(MetadataServerProviderInner::new(account_name))
20    }
21}
22
23/// [Provides tokens](https://cloud.google.com/compute/docs/instances/verifying-instance-identity)
24/// using the metadata server accessible when running from within GCP. Should not be used directly as it
25/// is not cached. Use `MetadataServerProvider` instead.
26#[derive(Debug)]
27pub struct MetadataServerProviderInner {
28    account_name: String,
29}
30
31impl MetadataServerProviderInner {
32    pub fn new(account_name: Option<String>) -> Self {
33        Self {
34            account_name: account_name.unwrap_or_else(|| "default".into()),
35        }
36    }
37}
38
39impl TokenProvider for MetadataServerProviderInner {
40    fn get_token_with_subject<'a, S, I, T>(
41        &self,
42        subject: Option<T>,
43        scopes: I,
44    ) -> Result<TokenOrRequest, Error>
45    where
46        S: AsRef<str> + 'a,
47        I: IntoIterator<Item = &'a S>,
48        T: Into<String>,
49    {
50        // We can only support subject being none
51        if subject.is_some() {
52            return Err(Error::Auth(error::AuthError {
53                error: Some("Unsupported".to_string()),
54                error_description: Some(
55                    "Metadata server tokens do not support jwt subjects".to_string(),
56                ),
57            }));
58        }
59
60        // Regardless of GCE or GAE, the token_uri is
61        // `computeMetadata/v1/instance/service-accounts/<name or id>/token`.
62        let mut url = format!("{}/{}/token", METADATA_URL, self.account_name);
63
64        // Merge all the scopes into a single string.
65        let scopes_str = scopes
66            .into_iter()
67            .map(|s| s.as_ref())
68            .collect::<Vec<_>>()
69            .join(",");
70
71        // If we have any scopes, pass them along in the querystring.
72        if !scopes_str.is_empty() {
73            url.push_str("?scopes=");
74            url.push_str(&scopes_str);
75        }
76
77        let request = http::Request::builder()
78            .method("GET")
79            .uri(url)
80            // To get responses from GCE, we must pass along the
81            // Metadata-Flavor header with a value of "Google".
82            .header("Metadata-Flavor", "Google")
83            .body(Vec::new())?;
84
85        Ok(TokenOrRequest::Request {
86            request,
87            reason: RequestReason::ParametersChanged,
88            scope_hash: 0,
89        })
90    }
91
92    fn parse_token_response<S>(
93        &self,
94        _hash: u64,
95        response: http::Response<S>,
96    ) -> Result<Token, Error>
97    where
98        S: AsRef<[u8]>,
99    {
100        let (parts, body) = response.into_parts();
101
102        if !parts.status.is_success() {
103            return Err(Error::HttpStatus(parts.status));
104        }
105
106        // Deserialize our response, or fail.
107        let token_res: TokenResponse = serde_json::from_slice(body.as_ref())?;
108
109        // Convert it into our output.
110        let token: Token = token_res.into();
111        Ok(token)
112    }
113}
114
115impl IdTokenProvider for MetadataServerProviderInner {
116    fn get_id_token(&self, audience: &str) -> Result<IdTokenOrRequest, error::Error> {
117        let url = format!(
118            "{}/{}/identity?audience={}",
119            METADATA_URL, self.account_name, audience,
120        );
121
122        let request = http::Request::builder()
123            .method("GET")
124            .uri(url)
125            .header("Metadata-Flavor", "Google")
126            .body(Vec::new())?;
127
128        Ok(IdTokenOrRequest::IdTokenRequest {
129            request,
130            reason: RequestReason::ParametersChanged,
131            audience_hash: 0,
132        })
133    }
134
135    fn parse_id_token_response<S>(
136        &self,
137        _hash: u64,
138        response: http::Response<S>,
139    ) -> Result<IdToken, Error>
140    where
141        S: AsRef<[u8]>,
142    {
143        let (parts, body) = response.into_parts();
144
145        if !parts.status.is_success() {
146            return Err(Error::HttpStatus(parts.status));
147        }
148
149        let token = IdToken::new(String::from_utf8_lossy(body.as_ref()).into_owned())?;
150
151        Ok(token)
152    }
153
154    fn get_id_token_with_access_token<S>(
155        &self,
156        _audience: &str,
157        _access_token_resp: crate::id_token::AccessTokenResponse<S>,
158    ) -> Result<crate::id_token::IdTokenRequest, Error>
159    where
160        S: AsRef<[u8]>,
161    {
162        // ID token via access token is not supported in the metadata service
163        // The token can be fetched directly via the metadataservice.
164        Err(Error::Auth(error::AuthError {
165            error: Some("Unsupported".to_string()),
166            error_description: Some(
167                "Metadata server id tokens via access token not supported".to_string(),
168            ),
169        }))
170    }
171}
172
173#[cfg(test)]
174mod test {
175    use super::*;
176
177    #[test]
178    fn metadata_noscopes() {
179        let provider = MetadataServerProvider::new(None);
180
181        let scopes: &[&str] = &[];
182
183        let token_or_req = provider
184            .get_token(scopes)
185            .expect("Should have gotten a request");
186
187        match token_or_req {
188            TokenOrRequest::Token(_) => panic!("Shouldn't have gotten a token"),
189            TokenOrRequest::Request { request, .. } => {
190                // Should be the metadata server
191                assert_eq!(request.uri().host(), Some("metadata.google.internal"));
192                // Since we had no scopes, no querystring.
193                assert_eq!(request.uri().query(), None);
194            }
195        }
196    }
197
198    #[test]
199    fn metadata_with_scopes() {
200        let provider = MetadataServerProvider::new(None);
201
202        let scopes = ["scope1", "scope2"];
203
204        let token_or_req = provider
205            .get_token(&scopes)
206            .expect("Should have gotten a request");
207
208        match token_or_req {
209            TokenOrRequest::Token(_) => panic!("Shouldn't have gotten a token"),
210            TokenOrRequest::Request { request, .. } => {
211                // Should be the metadata server
212                assert_eq!(request.uri().host(), Some("metadata.google.internal"));
213                // Since we had some scopes, we should have a querystring.
214                assert!(request.uri().query().is_some());
215
216                let query_string = request.uri().query().unwrap();
217                // We don't care about ordering, but the query_string
218                // should be comma-separated and only include the
219                // scopes.
220                assert!(
221                    query_string == "scopes=scope1,scope2"
222                        || query_string == "scopes=scope2,scope1"
223                );
224            }
225        }
226    }
227
228    #[test]
229    fn wrapper_dispatch() {
230        // Wrap the metadata server provider.
231        let provider =
232            crate::gcp::TokenProviderWrapperInner::Metadata(MetadataServerProviderInner::new(None));
233
234        // And then have the same test as metadata_with_scopes
235        let scopes = ["scope1", "scope2"];
236
237        let token_or_req = provider
238            .get_token(&scopes)
239            .expect("Should have gotten a request");
240
241        match token_or_req {
242            TokenOrRequest::Token(_) => panic!("Shouldn't have gotten a token"),
243            TokenOrRequest::Request { request, .. } => {
244                // Should be the metadata server
245                assert_eq!(request.uri().host(), Some("metadata.google.internal"));
246                // Since we had some scopes, we should have a querystring.
247                assert!(request.uri().query().is_some());
248
249                let query_string = request.uri().query().unwrap();
250                // We don't care about ordering, but the query_string
251                // should be comma-separated and only include the
252                // scopes.
253                assert!(
254                    query_string == "scopes=scope1,scope2"
255                        || query_string == "scopes=scope2,scope1"
256                );
257            }
258        }
259    }
260}