diff --git a/services/tunnelbroker/src/notifs/fcm/error.rs b/services/tunnelbroker/src/notifs/fcm/error.rs index 8e8885c61..4197782f9 100644 --- a/services/tunnelbroker/src/notifs/fcm/error.rs +++ b/services/tunnelbroker/src/notifs/fcm/error.rs @@ -1,15 +1,16 @@ use derive_more::{Display, Error, From}; #[derive(Debug, From, Display, Error)] pub enum Error { JWTError, ReqwestError(reqwest::Error), InvalidHeaderValue(reqwest::header::InvalidHeaderValue), SerdeJson(serde_json::Error), + FCMTokenNotInitialized, } impl From for Error { fn from(_: jsonwebtoken::errors::Error) -> Self { Self::JWTError } } diff --git a/services/tunnelbroker/src/notifs/fcm/mod.rs b/services/tunnelbroker/src/notifs/fcm/mod.rs index 760d659d7..9f743e7f1 100644 --- a/services/tunnelbroker/src/notifs/fcm/mod.rs +++ b/services/tunnelbroker/src/notifs/fcm/mod.rs @@ -1,20 +1,31 @@ use crate::notifs::fcm::config::FCMConfig; +use crate::notifs::fcm::token::FCMToken; +use std::time::Duration; pub mod config; mod error; +mod token; #[derive(Clone)] pub struct FCMClient { http_client: reqwest::Client, config: FCMConfig, + token: FCMToken, } impl FCMClient { pub fn new(config: &FCMConfig) -> Result { let http_client = reqwest::Client::builder().build()?; + + // Token must be a short-lived token (60 minutes) and in a reasonable + // timeframe. + let token_ttl = Duration::from_secs(60 * 55); + let token = FCMToken::new(&config.clone(), token_ttl)?; + Ok(FCMClient { http_client, config: config.clone(), + token, }) } } diff --git a/services/tunnelbroker/src/notifs/fcm/token.rs b/services/tunnelbroker/src/notifs/fcm/token.rs new file mode 100644 index 000000000..6bdcfeb20 --- /dev/null +++ b/services/tunnelbroker/src/notifs/fcm/token.rs @@ -0,0 +1,68 @@ +use crate::notifs::fcm::config::FCMConfig; +use crate::notifs::fcm::error::Error; +use crate::notifs::fcm::error::Error::FCMTokenNotInitialized; +use jsonwebtoken::{Algorithm, EncodingKey, Header}; +use serde::Deserialize; +use serde_json::json; +use std::sync::Arc; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use tokio::sync::RwLock; +use tracing::debug; + +#[derive(Debug, Clone, Deserialize)] +struct FCMAccessToken { + access_token: String, + token_type: String, + expiration_time: u64, +} + +#[derive(Debug, Clone)] +pub struct FCMToken { + token: Arc>>, + config: FCMConfig, + validity_duration: Duration, +} + +impl FCMToken { + pub fn new(config: &FCMConfig, token_ttl: Duration) -> Result { + Ok(FCMToken { + token: Arc::new(RwLock::new(None)), + config: config.clone(), + validity_duration: token_ttl, + }) + } + + pub async fn get_auth_bearer(&self) -> Result { + let bearer = self.token.read().await; + match &*bearer { + Some(token) => Ok(format!("{} {}", token.token_type, token.access_token)), + None => Err(FCMTokenNotInitialized), + } + } + + fn get_jwt_token(&self, created_at: u64) -> Result { + let exp = created_at + self.validity_duration.as_secs(); + let payload = json!({ + // The email address of the service account. + "iss": self.config.client_email, + // A descriptor of the intended target of the assertion. + "aud": self.config.token_uri, + // The time the assertion was issued. + "iat": created_at, + // The expiration time of the assertion. + // This value has a maximum of 1 hour after the issued time. + "exp": exp, + // A space-delimited list of the permissions that the application + // requests. + "scope": "https://www.googleapis.com/auth/firebase.messaging", + }); + + debug!("Encoding JWT token for FCM, created at: {}", created_at); + + let header = Header::new(Algorithm::RS256); + let encoding_key = + EncodingKey::from_rsa_pem(self.config.private_key.as_bytes()).unwrap(); + let token = jsonwebtoken::encode(&header, &payload, &encoding_key)?; + Ok(token) + } +}