Skip to main content

yup_oauth2/
service_account.rs

1//! This module provides a flow that obtains tokens for service accounts.
2//!
3//! Service accounts are usually used by software (i.e., non-human actors) to get access to
4//! resources. Currently, this module only works with RS256 JWTs, which makes it at least suitable
5//! for authentication with Google services.
6//!
7//! Resources:
8//! - [Using OAuth 2.0 for Server to Server
9//!   Applications](https://developers.google.com/identity/protocols/OAuth2ServiceAccount)
10//! - [JSON Web Tokens](https://jwt.io/)
11//!
12//! Copyright (c) 2016 Google Inc (lewinb@google.com).
13
14use crate::client::SendRequest;
15use crate::error::Error;
16use crate::types::TokenInfo;
17
18use std::{io, path::PathBuf};
19
20use base64::Engine as _;
21
22use http::header;
23use http_body_util::BodyExt;
24#[cfg(all(feature = "aws-lc-rs", not(feature = "ring")))]
25use rustls::crypto::aws_lc_rs as crypto_provider;
26#[cfg(feature = "ring")]
27use rustls::crypto::ring as crypto_provider;
28use rustls::pki_types::pem::PemObject;
29use rustls::{self, pki_types::PrivateKeyDer, sign::SigningKey};
30use serde::{Deserialize, Serialize};
31use time::OffsetDateTime;
32use url::form_urlencoded;
33
34const GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:jwt-bearer";
35const GOOGLE_RS256_HEAD: &str = r#"{"alg":"RS256","typ":"JWT"}"#;
36
37/// Encodes s as Base64
38fn append_base64<T: AsRef<[u8]> + ?Sized>(s: &T, out: &mut String) {
39    base64::engine::general_purpose::URL_SAFE.encode_string(s, out)
40}
41
42/// JSON schema of secret service account key.
43///
44/// You can obtain the key from the [Cloud Console](https://console.cloud.google.com/).
45///
46/// You can use `helpers::read_service_account_key()` as a quick way to read a JSON client
47/// secret into a ServiceAccountKey.
48#[derive(Serialize, Deserialize, Debug, Clone)]
49pub struct ServiceAccountKey {
50    #[serde(rename = "type")]
51    /// key_type
52    pub key_type: Option<String>,
53    /// project_id
54    pub project_id: Option<String>,
55    /// private_key_id
56    pub private_key_id: Option<String>,
57    /// private_key
58    pub private_key: String,
59    /// client_email
60    pub client_email: String,
61    /// client_id
62    pub client_id: Option<String>,
63    /// auth_uri
64    pub auth_uri: Option<String>,
65    /// token_uri
66    pub token_uri: String,
67    /// auth_provider_x509_cert_url
68    pub auth_provider_x509_cert_url: Option<String>,
69    /// client_x509_cert_url
70    pub client_x509_cert_url: Option<String>,
71}
72
73/// Permissions requested for a JWT.
74/// See https://developers.google.com/identity/protocols/OAuth2ServiceAccount#authorizingrequests.
75#[derive(Serialize, Debug)]
76struct Claims<'a> {
77    iss: &'a str,
78    aud: &'a str,
79    exp: i64,
80    iat: i64,
81    #[serde(rename = "sub")]
82    subject: Option<&'a str>,
83    scope: String,
84}
85
86impl<'a> Claims<'a> {
87    fn new<T>(key: &'a ServiceAccountKey, scopes: &[T], subject: Option<&'a str>) -> Self
88    where
89        T: AsRef<str>,
90    {
91        let iat = OffsetDateTime::now_utc().unix_timestamp();
92        let expiry = iat + 3600 - 5; // Max validity is 1h.
93
94        let scope = crate::helper::join(scopes, " ");
95        Claims {
96            iss: &key.client_email,
97            aud: &key.token_uri,
98            exp: expiry,
99            iat,
100            subject,
101            scope,
102        }
103    }
104}
105
106/// A JSON Web Token ready for signing.
107pub(crate) struct JWTSigner {
108    signer: Box<dyn rustls::sign::Signer>,
109}
110
111impl JWTSigner {
112    fn new(private_key: &str) -> Result<Self, io::Error> {
113        let key = PrivateKeyDer::from_pem_slice(private_key.as_bytes()).map_err(|_| {
114            io::Error::new(io::ErrorKind::InvalidInput, "Error reading key from PEM")
115        })?;
116        let signing_key = crypto_provider::sign::RsaSigningKey::new(&key)
117            .map_err(|_| io::Error::other("Couldn't initialize signer"))?;
118        let signer = signing_key
119            .choose_scheme(&[rustls::SignatureScheme::RSA_PKCS1_SHA256])
120            .ok_or_else(|| io::Error::other("Couldn't choose signing scheme"))?;
121        Ok(JWTSigner { signer })
122    }
123
124    fn sign_claims(&self, claims: &Claims) -> Result<String, rustls::Error> {
125        let mut jwt_head = Self::encode_claims(claims);
126        let signature = self.signer.sign(jwt_head.as_bytes())?;
127        jwt_head.push('.');
128        append_base64(&signature, &mut jwt_head);
129        Ok(jwt_head)
130    }
131
132    /// Encodes the first two parts (header and claims) to base64 and assembles them into a form
133    /// ready to be signed.
134    fn encode_claims(claims: &Claims) -> String {
135        let mut head = String::new();
136        append_base64(GOOGLE_RS256_HEAD, &mut head);
137        head.push('.');
138        append_base64(&serde_json::to_string(&claims).unwrap(), &mut head);
139        head
140    }
141}
142
143pub struct ServiceAccountFlowOpts {
144    pub(crate) key: FlowOptsKey,
145    pub(crate) subject: Option<String>,
146}
147
148/// The source of the key given to ServiceAccountFlowOpts.
149pub(crate) enum FlowOptsKey {
150    /// A path at which the key can be read from disk
151    Path(PathBuf),
152    /// An already initialized key
153    Key(Box<ServiceAccountKey>),
154}
155
156/// ServiceAccountFlow can fetch oauth tokens using a service account.
157pub struct ServiceAccountFlow {
158    key: ServiceAccountKey,
159    subject: Option<String>,
160    signer: JWTSigner,
161}
162
163impl ServiceAccountFlow {
164    pub(crate) async fn new(opts: ServiceAccountFlowOpts) -> Result<Self, io::Error> {
165        let key = match opts.key {
166            FlowOptsKey::Path(path) => crate::read_service_account_key(path).await?,
167            FlowOptsKey::Key(key) => *key,
168        };
169
170        let signer = JWTSigner::new(&key.private_key)?;
171        Ok(ServiceAccountFlow {
172            key,
173            subject: opts.subject,
174            signer,
175        })
176    }
177
178    /// Send a request for a new Bearer token to the OAuth provider.
179    pub(crate) async fn token<T>(
180        &self,
181        hyper_client: &impl SendRequest,
182        scopes: &[T],
183    ) -> Result<TokenInfo, Error>
184    where
185        T: AsRef<str>,
186    {
187        let claims = Claims::new(&self.key, scopes, self.subject.as_deref());
188        let signed = self
189            .signer
190            .sign_claims(&claims)
191            .map_err(|_| Error::LowLevelError(io::Error::other("unable to sign claims")))?;
192        let rqbody = form_urlencoded::Serializer::new(String::new())
193            .extend_pairs(&[("grant_type", GRANT_TYPE), ("assertion", signed.as_str())])
194            .finish();
195        let request = http::Request::post(&self.key.token_uri)
196            .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
197            .body(rqbody)
198            .unwrap();
199        log::debug!("requesting token from service account: {:?}", request);
200        let (head, body) = hyper_client.request(request).await?.into_parts();
201        let body = body.collect().await?.to_bytes();
202        log::debug!("received response; head: {:?}, body: {:?}", head, body);
203        TokenInfo::from_json(&body)
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210    use crate::helper::read_service_account_key;
211
212    // Valid but deactivated key.
213    const TEST_PRIVATE_KEY_PATH: &str = "examples/Sanguine-69411a0c0eea.json";
214
215    // Uncomment this test to verify that we can successfully obtain tokens.
216    #[cfg(feature = "hyper-rustls")]
217    // #[tokio::test]
218    #[allow(dead_code)]
219    async fn test_service_account_e2e() {
220        let acc = ServiceAccountFlow::new(ServiceAccountFlowOpts {
221            key: FlowOptsKey::Path(TEST_PRIVATE_KEY_PATH.into()),
222            subject: None,
223        })
224        .await
225        .unwrap();
226        let client = crate::client::HttpClient::new(
227            hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new())
228                .build(
229                    hyper_rustls::HttpsConnectorBuilder::new()
230                        .with_provider_and_native_roots(crypto_provider::default_provider())
231                        .unwrap()
232                        .https_only()
233                        .enable_http1()
234                        .enable_http2()
235                        .build(),
236                ),
237            None,
238        );
239        println!(
240            "{:?}",
241            acc.token(&client, &["https://www.googleapis.com/auth/pubsub"])
242                .await
243        );
244        println!(
245            "{:?}",
246            acc.token(
247                &client,
248                &["https://some.scope/likely-to-hand-out-id-tokens"]
249            )
250            .await
251        );
252    }
253
254    #[tokio::test]
255    async fn test_jwt_initialize_claims() {
256        let key = read_service_account_key(TEST_PRIVATE_KEY_PATH)
257            .await
258            .unwrap();
259        let scopes = vec!["scope1", "scope2", "scope3"];
260        let claims = Claims::new(&key, &scopes, None);
261
262        assert_eq!(
263            claims.iss,
264            "oauth2-public-test@sanguine-rhythm-105020.iam.gserviceaccount.com".to_string()
265        );
266        assert_eq!(claims.scope, "scope1 scope2 scope3".to_string());
267        assert_eq!(
268            claims.aud,
269            "https://accounts.google.com/o/oauth2/token".to_string()
270        );
271        assert!(claims.exp > 1000000000);
272        assert!(claims.iat < claims.exp);
273        assert_eq!(claims.exp - claims.iat, 3595);
274    }
275
276    #[tokio::test]
277    async fn test_jwt_sign() {
278        let key = read_service_account_key(TEST_PRIVATE_KEY_PATH)
279            .await
280            .unwrap();
281        let scopes = vec!["scope1", "scope2", "scope3"];
282        let signer = JWTSigner::new(&key.private_key).unwrap();
283        let claims = Claims::new(&key, &scopes, None);
284        let signature = signer.sign_claims(&claims);
285
286        assert!(signature.is_ok());
287
288        let signature = signature.unwrap();
289        assert_eq!(
290            signature.split('.').next().unwrap(),
291            "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9"
292        );
293    }
294}