tame_oauth/gcp/
end_user.rs

1use super::TokenResponse;
2use crate::{
3    error::{self, Error},
4    id_token::{
5        AccessTokenResponse, IdTokenOrRequest, IdTokenProvider, IdTokenRequest, IdTokenResponse,
6    },
7    token::{RequestReason, Token, TokenOrRequest, TokenProvider},
8    token_cache::CachedTokenProvider,
9    IdToken,
10};
11
12/// Provides tokens using
13/// [default application credentials](https://cloud.google.com/sdk/gcloud/reference/auth/application-default)
14/// Caches tokens internally.
15pub type EndUserCredentials = CachedTokenProvider<EndUserCredentialsInner>;
16impl EndUserCredentials {
17    pub fn new(info: EndUserCredentialsInfo) -> Self {
18        CachedTokenProvider::wrap(EndUserCredentialsInner::new(info))
19    }
20}
21
22/// Provides tokens using
23/// [default application credentials](https://cloud.google.com/sdk/gcloud/reference/auth/application-default)
24#[derive(serde::Deserialize, Debug, Clone)]
25pub struct EndUserCredentialsInfo {
26    /// The OAuth2 client_id
27    pub client_id: String,
28    /// The OAuth2 client_secret
29    pub client_secret: String,
30    /// The OAuth2 refresh_token
31    pub refresh_token: String,
32    /// The client type (the value must be authorized_user)
33    #[serde(rename = "type")]
34    pub client_type: String,
35}
36
37impl EndUserCredentialsInfo {
38    /// Deserializes the `EndUserCredentials` from a byte slice. This
39    /// data is typically acquired by reading an
40    /// `application_default_credentials.json` file from disk.
41    pub fn deserialize<T>(key_data: T) -> Result<Self, Error>
42    where
43        T: AsRef<[u8]>,
44    {
45        let slice = key_data.as_ref();
46
47        let account_info: Self = serde_json::from_slice(slice)?;
48        Ok(account_info)
49    }
50}
51
52/// A token provider for
53/// [default application credentials](https://cloud.google.com/sdk/gcloud/reference/auth/application-default)
54/// Should not be used directly as it is not cached. Use `EndUserCredentials` instead.
55pub struct EndUserCredentialsInner {
56    info: EndUserCredentialsInfo,
57}
58
59impl std::fmt::Debug for EndUserCredentialsInner {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        f.debug_struct("EndUserCredentialsInner")
62            .finish_non_exhaustive()
63    }
64}
65
66impl EndUserCredentialsInner {
67    pub fn new(info: EndUserCredentialsInfo) -> Self {
68        Self { info }
69    }
70}
71
72#[derive(serde::Deserialize, Debug)]
73struct IdTokenResponseBody {
74    /// The actual token
75    id_token: String,
76}
77
78impl EndUserCredentialsInner {
79    fn prepare_token_request(&self) -> Result<http::Request<Vec<u8>>, Error> {
80        // To get an access token or id_token, we need to perform a refresh
81        // following the instructions at
82        // https://developers.google.com/identity/protocols/oauth2/web-server#offline
83        // (i.e., POST our client data as a refresh_token request to
84        // the /token endpoint).
85        // The response will include both a access token and a id token
86        let url = "https://oauth2.googleapis.com/token";
87
88        // Build up the parameters as a form encoded string.
89        let body = url::form_urlencoded::Serializer::new(String::new())
90            .append_pair("client_id", &self.info.client_id)
91            .append_pair("client_secret", &self.info.client_secret)
92            .append_pair("grant_type", "refresh_token")
93            .append_pair("refresh_token", &self.info.refresh_token)
94            .finish();
95
96        let body = Vec::from(body);
97
98        let request = http::Request::builder()
99            .method("POST")
100            .uri(url)
101            .header(
102                http::header::CONTENT_TYPE,
103                "application/x-www-form-urlencoded",
104            )
105            .header(http::header::CONTENT_LENGTH, body.len())
106            .body(body)?;
107
108        Ok(request)
109    }
110}
111
112impl TokenProvider for EndUserCredentialsInner {
113    fn get_token_with_subject<'a, S, I, T>(
114        &self,
115        subject: Option<T>,
116        // EndUserCredentials only have the scopes they were granted
117        // via their authorization. So whatever scopes you're asking
118        // for, better have been handled when authorized. `gcloud auth
119        // application-default login` will get the
120        // https://www.googleapis.com/auth/cloud-platform which
121        // includes all *GCP* APIs.
122        _scopes: I,
123    ) -> Result<TokenOrRequest, Error>
124    where
125        S: AsRef<str> + 'a,
126        I: IntoIterator<Item = &'a S>,
127        T: Into<String>,
128    {
129        // We can only support subject being none
130        if subject.is_some() {
131            return Err(Error::Auth(error::AuthError {
132                error: Some("Unsupported".to_string()),
133                error_description: Some(
134                    "ADC / User tokens do not support jwt subjects".to_string(),
135                ),
136            }));
137        }
138
139        let request = self.prepare_token_request()?;
140
141        Ok(TokenOrRequest::Request {
142            request,
143            reason: RequestReason::ParametersChanged,
144            scope_hash: 0,
145        })
146    }
147
148    fn parse_token_response<S>(
149        &self,
150        _hash: u64,
151        response: http::Response<S>,
152    ) -> Result<Token, Error>
153    where
154        S: AsRef<[u8]>,
155    {
156        let (parts, body) = response.into_parts();
157
158        if !parts.status.is_success() {
159            return Err(Error::HttpStatus(parts.status));
160        }
161
162        // Deserialize our response, or fail.
163        let token_res: TokenResponse = serde_json::from_slice(body.as_ref())?;
164
165        // TODO(boulos): The response also includes the set of scopes
166        // (as "scope") that we're granted. We could check that
167        // cloud-platform is in it.
168
169        // Convert it into our output.
170        let token: Token = token_res.into();
171        Ok(token)
172    }
173}
174
175impl IdTokenProvider for EndUserCredentialsInner {
176    fn get_id_token(&self, _audience: &str) -> Result<IdTokenOrRequest, Error> {
177        let request = self.prepare_token_request()?;
178
179        Ok(IdTokenOrRequest::IdTokenRequest {
180            request,
181            reason: RequestReason::ParametersChanged,
182            audience_hash: 0,
183        })
184    }
185
186    fn get_id_token_with_access_token<S>(
187        &self,
188        _audience: &str,
189        _response: AccessTokenResponse<S>,
190    ) -> Result<IdTokenRequest, Error>
191    where
192        S: AsRef<[u8]>,
193    {
194        // ID token via access token is not supported with user credentials
195        // The token is fetched via the same token request as the access token
196        Err(Error::Auth(error::AuthError {
197            error: Some("Unsupported".to_string()),
198            error_description: Some(
199                "User credentials id tokens via access token not supported".to_string(),
200            ),
201        }))
202    }
203
204    fn parse_id_token_response<S>(
205        &self,
206        _hash: u64,
207        response: IdTokenResponse<S>,
208    ) -> Result<IdToken, Error>
209    where
210        S: AsRef<[u8]>,
211    {
212        let (parts, body) = response.into_parts();
213
214        if !parts.status.is_success() {
215            let body_bytes = body.as_ref();
216
217            if parts
218                .headers
219                .get(http::header::CONTENT_TYPE)
220                .and_then(|ct| ct.to_str().ok())
221                == Some("application/json; charset=utf-8")
222            {
223                if let Ok(auth_error) = serde_json::from_slice::<error::AuthError>(body_bytes) {
224                    return Err(Error::Auth(auth_error));
225                }
226            }
227
228            return Err(Error::HttpStatus(parts.status));
229        }
230
231        let token_res: IdTokenResponseBody = serde_json::from_slice(body.as_ref())?;
232        let token = IdToken::new(token_res.id_token)?;
233
234        Ok(token)
235    }
236}
237
238#[cfg(test)]
239mod test {
240    use super::*;
241
242    #[test]
243    fn end_user_credentials() {
244        let provider = EndUserCredentialsInner::new(EndUserCredentialsInfo {
245            client_id: "fake_client@domain.com".into(),
246            client_secret: "TOP_SECRET".into(),
247            refresh_token: "REFRESH_TOKEN".into(),
248            client_type: "authorized_user".into(),
249        });
250
251        // End-user credentials don't let you override scopes.
252        let scopes = vec!["better_not_be_there"];
253
254        let token_or_req = provider
255            .get_token(&scopes)
256            .expect("Should have gotten a request");
257
258        match token_or_req {
259            TokenOrRequest::Token(_) => panic!("Shouldn't have gotten a token"),
260            TokenOrRequest::Request { request, .. } => {
261                // Should be the Google oauth2 API
262                assert_eq!(request.uri().host(), Some("oauth2.googleapis.com"));
263                // Scopes aren't passed for end user credentials
264                assert_eq!(request.uri().query(), None);
265            }
266        }
267    }
268}