Master Google OAuth 2.0 in Rust with Service Account Credentials
This article explains how to implement Google OAuth 2.0 authentication in Rust using service account credentials, covering JWT generation, token management, scope and subject configuration, and demonstrates a simple translation service built on the Google Cloud Translation API.
Background
When developing backend applications in Rust, service accounts are often used for server‑to‑server authentication with Google APIs. This guide shows how to generate a JWT, request an access token, and manage token expiration.
Service Account Credentials Structure
A ServiceAccountCredentials struct stores the fields read from a JSON key file, as well as optional fields for the access token, scopes, and subject.
<code>#[derive(Debug, Clone)]
pub struct ServiceAccountCredentials {
r#type: String,
project_id: String,
private_key_id: String,
private_key: String,
client_email: String,
client_id: String,
auth_uri: String,
token_uri: String,
auth_provider_x509_cert_url: String,
client_x509_cert_url: String,
universe_domain: String,
#[serde(skip_serializing_if = "Option::is_none")]
token: Option<Token>,
#[serde(skip_serializing_if = "Option::is_none")]
scopes: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
sub: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Token {
expiration_time: i64,
access_token: String,
}</code>Creating a ServiceAccountCredentials Instance
You can create an instance either from a JSON file or from a JSON string:
Read the JSON file and deserialize it.
<code>impl ServiceAccountCredentials {
pub fn from_service_account_file(filepath: PathBuf) -> Result<Self> {
let credentials_json = fs::read_to_string(filepath)?;
Ok(serde_json::from_str::<ServiceAccountCredentials>(&credentials_json)?)
}
}
</code>Parse a JSON string directly.
<code>impl ServiceAccountCredentials {
pub fn from_service_account_info(credentials_json: String) -> Result<Self> {
Ok(serde_json::from_str::<ServiceAccountCredentials>(&credentials_json)?)
}
}
</code>Setting the Subject (sub)
Some APIs, such as Gmail, require the sub claim in the JWT. Use with_subject to set it, which also clears the cached token so a new JWT is generated.
<code>pub fn with_subject(&self, subject: &str) -> Self {
let mut subjected_credential = self.clone();
subjected_credential.sub = Some(subject.to_owned());
subjected_credential.token = None;
subjected_credential
}
</code>Setting Scopes
Define the OAuth 2.0 scopes required for the target Google API with with_scopes . Changing scopes also clears the cached token.
<code>pub fn with_scopes(&self, scopes: Vec<&str>) -> Self {
let mut scoped_credentials = self.clone();
scoped_credentials.scopes = Some(scopes.into_iter().map(|s| s.to_owned()).collect());
scoped_credentials.token = None;
scoped_credentials
}
</code>Getting an Access Token
The get_access_token method checks whether a valid token is cached; if not, it creates a new JWT, requests a token from Google, caches it with a one‑hour expiration, and returns the token.
<code>pub async fn get_access_token(&mut self) -> Result<String> {
let now = Local::now();
let iat = now.timestamp();
match self.token.clone() {
Some(token) => {
if iat > token.expiration_time {
let jwt = self.make_assertion()?;
let access_token = self.request_token(&jwt).await?;
self.token = Some(Token { expiration_time: (now + Duration::minutes(58)).timestamp(), access_token: access_token.clone() });
Ok(access_token)
} else {
Ok(token.access_token.clone())
}
}
None => {
let jwt = self.make_assertion()?;
let access_token = self.request_token(&jwt).await?;
self.token = Some(Token { expiration_time: (now + Duration::minutes(58)).timestamp(), access_token: access_token.clone() });
Ok(access_token)
}
}
}
</code>The helper methods make_assertion builds the JWT and request_token performs the HTTP POST to the token endpoint.
Example: Translation Service
A simple TranslateService struct demonstrates how to combine the credential handling with the Google Cloud Translation API.
<code>static TRANSLATE_SERVICE_SCOPE: &str = "https://www.googleapis.com/auth/cloud-translation";
static TRANSLATE_SERVICE_BASE_URL: &str = "https://translation.googleapis.com/language/translate";
#[derive(Debug, Clone)]
pub struct TranslateService {
api_key: Option<String>,
service_account_credentials: Option<ServiceAccountCredentials>,
}
impl TranslateService {
pub fn new_with_api_key(api_key: String) -> Self { Self { api_key: Some(api_key), service_account_credentials: None } }
pub fn new_with_credentials(service_account_credentials: ServiceAccountCredentials) -> Self {
let scoped_credentials = service_account_credentials.with_scopes(vec![TRANSLATE_SERVICE_SCOPE]);
Self { api_key: None, service_account_credentials: Some(scoped_credentials) }
}
pub async fn translate(&mut self, text: Vec<&str>, target: &str) -> Result<TranslateTextResponse> {
// Build request URL and headers, obtain token if needed, then call the API.
// ... (omitted for brevity)
}
}
</code>Data structures for the request and response are defined with Serde for JSON (e.g., TranslateTextRequest , TranslateTextResponse ).
Summary
The article shows how to perform Google OAuth 2.0 authentication in Rust using service account credentials, manage JWT creation, token caching, scopes, and subjects, and provides a concrete example of a translation service that automatically obtains access tokens and calls the Cloud Translation API.
Architecture Development Notes
Focused on architecture design, technology trend analysis, and practical development experience sharing.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.