Skip to main content

google_cloud_auth/credentials/
api_key_credentials.rs

1// Copyright 2025 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! [API Key] Credentials type.
16//!
17//! An API key is a simple encrypted string that you can use when calling
18//! Google Cloud APIs. When you use API keys in your applications, ensure that
19//! they are kept secure during both storage and transmission.
20//!
21//! [API Key]: https://cloud.google.com/api-keys/docs/overview
22
23use crate::credentials::dynamic::CredentialsProvider;
24use crate::credentials::{CacheableResource, Credentials, Result};
25use crate::headers_util::AuthHeadersBuilder;
26use crate::token::{CachedTokenProvider, Token, TokenProvider};
27use crate::token_cache::TokenCache;
28use http::{Extensions, HeaderMap};
29use std::sync::Arc;
30
31struct ApiKeyTokenProvider {
32    api_key: String,
33}
34
35impl std::fmt::Debug for ApiKeyTokenProvider {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        f.debug_struct("ApiKeyCredentials")
38            .field("api_key", &"[censored]")
39            .finish()
40    }
41}
42
43#[async_trait::async_trait]
44impl TokenProvider for ApiKeyTokenProvider {
45    async fn token(&self) -> Result<Token> {
46        Ok(Token {
47            token: self.api_key.clone(),
48            token_type: String::new(),
49            expires_at: None,
50            metadata: None,
51        })
52    }
53}
54
55#[derive(Debug)]
56struct ApiKeyCredentials<T>
57where
58    T: CachedTokenProvider,
59{
60    token_provider: T,
61}
62
63/// A builder for creating credentials that authenticate using an [API key].
64///
65/// API keys are convenient because no [principal] is needed. The API key
66/// associates the request with a Google Cloud project for billing and quota
67/// purposes.
68///
69/// Note that only some Cloud APIs support API keys. The rest require full
70/// credentials.
71///
72/// [API key]: https://cloud.google.com/docs/authentication/api-keys-use
73/// [principal]: https://cloud.google.com/docs/authentication#principal
74#[derive(Debug)]
75pub struct Builder {
76    api_key: String,
77}
78
79impl Builder {
80    /// Creates a new builder with given API key.
81    ///
82    /// # Example
83    /// ```
84    /// # use google_cloud_auth::credentials::api_key_credentials::Builder;
85    /// # async fn sample() {
86    /// let credentials = Builder::new("my-api-key").build();
87    /// # }
88    /// ```
89    pub fn new<T: Into<String>>(api_key: T) -> Self {
90        Self {
91            api_key: api_key.into(),
92        }
93    }
94
95    fn build_token_provider(self) -> ApiKeyTokenProvider {
96        ApiKeyTokenProvider {
97            api_key: self.api_key,
98        }
99    }
100
101    /// Returns a [Credentials] instance with the configured settings.
102    pub fn build(self) -> Credentials {
103        Credentials {
104            inner: Arc::new(ApiKeyCredentials {
105                token_provider: TokenCache::new(self.build_token_provider()),
106            }),
107        }
108    }
109}
110
111#[async_trait::async_trait]
112impl<T> CredentialsProvider for ApiKeyCredentials<T>
113where
114    T: CachedTokenProvider,
115{
116    async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
117        let cached_token = self.token_provider.token(extensions).await?;
118        AuthHeadersBuilder::for_api_key(&cached_token).build()
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use crate::credentials::tests::get_headers_from_cache;
126    use http::HeaderValue;
127    use scoped_env::ScopedEnv;
128    use serial_test::{parallel, serial};
129
130    const API_KEY_HEADER_KEY: &str = "x-goog-api-key";
131    type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
132
133    #[test]
134    #[parallel]
135    fn debug_token_provider() {
136        let expected = Builder::new("test-api-key").build_token_provider();
137
138        let fmt = format!("{expected:?}");
139        assert!(!fmt.contains("super-secret-api-key"), "{fmt}");
140    }
141
142    #[tokio::test]
143    #[parallel]
144    async fn api_key_credentials_token_provider() {
145        let tp = Builder::new("test-api-key").build_token_provider();
146        assert_eq!(
147            tp.token().await.unwrap(),
148            Token {
149                token: "test-api-key".to_string(),
150                token_type: String::new(),
151                expires_at: None,
152                metadata: None,
153            }
154        );
155    }
156
157    #[tokio::test]
158    #[serial]
159    async fn create_api_key_credentials_basic() -> TestResult {
160        let _e = ScopedEnv::remove("GOOGLE_CLOUD_QUOTA_PROJECT");
161
162        let creds = Builder::new("test-api-key").build();
163        let headers = get_headers_from_cache(creds.headers(Extensions::new()).await.unwrap())?;
164        let value = headers.get(API_KEY_HEADER_KEY).unwrap();
165
166        assert_eq!(headers.len(), 1, "{headers:?}");
167        assert_eq!(value, HeaderValue::from_static("test-api-key"));
168        assert!(value.is_sensitive());
169        Ok(())
170    }
171
172    #[tokio::test]
173    #[serial]
174    async fn create_api_key_credentials_basic_with_extensions() -> TestResult {
175        let _e = ScopedEnv::remove("GOOGLE_CLOUD_QUOTA_PROJECT");
176
177        let creds = Builder::new("test-api-key").build();
178        let mut extensions = Extensions::new();
179        let cached_headers = creds.headers(extensions.clone()).await?;
180        let (headers, entity_tag) = match cached_headers {
181            CacheableResource::New { entity_tag, data } => (data, entity_tag),
182            CacheableResource::NotModified => unreachable!("expecting new headers"),
183        };
184        let value = headers.get(API_KEY_HEADER_KEY).unwrap();
185
186        assert_eq!(headers.len(), 1, "{headers:?}");
187        assert_eq!(value, HeaderValue::from_static("test-api-key"));
188        assert!(value.is_sensitive());
189        extensions.insert(entity_tag);
190
191        let cached_headers = creds.headers(extensions).await?;
192
193        match cached_headers {
194            CacheableResource::New { .. } => unreachable!("expecting new headers"),
195            CacheableResource::NotModified => CacheableResource::<HeaderMap>::NotModified,
196        };
197
198        Ok(())
199    }
200}