service_authenticator/
service_account.rs

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