use crate::error::{FcmError, NetworkError, ResultMapError};
use jsonwebtoken::{encode, EncodingKey, Header};
use reqwest::Client;
use serde::Deserialize;
use serde_json::json;
use std::time::{Duration, Instant, SystemTime};
use tracing::{debug, info, instrument};
pub type SharedTokenManager = std::sync::Arc<tokio::sync::Mutex<TokenManager>>;
pub struct TokenManager {
token: Option<String>,
expires_at: Option<Instant>,
service_account_key: ServiceAccountKey,
}
#[derive(Deserialize, Debug)]
struct ServiceAccountKey {
private_key: String,
client_email: String,
private_key_id: String,
}
impl TokenManager {
#[instrument(level = "info")]
pub fn new(google_credentials_location: &str) -> Result<Self, FcmError> {
info!("Creating new TokenManager");
let service_account_key_json = std::fs::read_to_string(google_credentials_location)?;
let service_account_key: ServiceAccountKey =
serde_json::from_str(&service_account_key_json)?;
Ok(TokenManager {
token: None,
expires_at: None,
service_account_key,
})
}
#[instrument(level = "debug", skip(self))]
pub async fn get_token(&mut self) -> Result<String, FcmError> {
if let Some(token) = &self.token {
if !self.is_token_expired() {
debug!("Using cached token");
return Ok(token.clone());
}
}
debug!("Refreshing token");
self.refresh_token().await
}
#[instrument(level = "debug", skip(self))]
pub fn is_token_expired(&self) -> bool {
self.expires_at
.map(|expires_at| {
let expired = expires_at <= Instant::now();
debug!("Token expired: {}", expired);
expired
})
.unwrap_or(true)
}
#[instrument(level = "info", skip(self))]
pub async fn refresh_token(&mut self) -> Result<String, FcmError> {
info!("Refreshing token");
self.refresh_token_with_url("https://oauth2.googleapis.com/token")
.await
}
#[instrument(level = "info", skip(self))]
pub async fn refresh_token_with_url(
&mut self,
auth_server_url: &str,
) -> Result<String, FcmError> {
info!("Refreshing token with URL: {}", auth_server_url);
let signed_jwt = create_signed_jwt(&self.service_account_key)?;
let access_token_response = get_access_token(&signed_jwt, auth_server_url).await?;
let new_token = access_token_response.access_token;
self.token = Some(new_token.clone());
self.expires_at =
Some(Instant::now() + Duration::from_secs(access_token_response.expires_in));
info!("Token refreshed successfully");
Ok(new_token)
}
}
#[instrument(level = "debug")]
fn create_signed_jwt(service_account_key: &ServiceAccountKey) -> Result<String, FcmError> {
debug!("Creating signed JWT");
let mut header = Header::new(jsonwebtoken::Algorithm::RS256);
header.kid = Some(service_account_key.private_key_id.clone());
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or(Duration::new(0, 0))
.as_secs();
let claims = json!({
"iss": service_account_key.client_email,
"scope": "https://www.googleapis.com/auth/firebase.messaging",
"aud": "https://oauth2.googleapis.com/token",
"exp": now + 3600,
"iat": now
});
let encoding_key = EncodingKey::from_rsa_pem(service_account_key.private_key.as_bytes())?;
let signed_jwt = encode(&header, &claims, &encoding_key).map_err(FcmError::JwtEncodeError)?;
debug!("Signed JWT created");
Ok(signed_jwt)
}
#[derive(Deserialize)]
struct AccessTokenResponse {
access_token: String,
expires_in: u64,
}
#[instrument(level = "debug")]
async fn get_access_token(
signed_jwt: &str,
auth_url: &str,
) -> Result<AccessTokenResponse, FcmError> {
debug!("Getting access token from: {}", auth_url);
let client = Client::new();
let params = [
("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"),
("assertion", signed_jwt),
];
let response = client
.post(auth_url)
.form(¶ms)
.send()
.await
.map_err(NetworkError::SendRequestError)
.map_oauth_err()?;
debug!("Response status: {}", response.status());
let access_token_response = response
.json::<AccessTokenResponse>()
.await
.map_err(NetworkError::ResponseError)
.map_oauth_err()?;
debug!("Access token obtained");
Ok(access_token_response)
}