From 5c86d4e899304fa78870201198ed092ccb153b3e Mon Sep 17 00:00:00 2001 From: Denis Nutiu Date: Sat, 28 Dec 2024 13:26:16 +0200 Subject: [PATCH] implement JWT token --- bot/Cargo.toml | 3 + bot/src/bluesky.rs | 1 + bot/src/bluesky/token.rs | 131 +++++++++++++++++++++++++++++++++++++++ bot/src/main.rs | 1 + 4 files changed, 136 insertions(+) create mode 100644 bot/src/bluesky.rs create mode 100644 bot/src/bluesky/token.rs diff --git a/bot/Cargo.toml b/bot/Cargo.toml index 889bb27..601fe32 100644 --- a/bot/Cargo.toml +++ b/bot/Cargo.toml @@ -12,3 +12,6 @@ signal-hook = "0.3.17" log = "0.4.22" env_logger = "0.11.6" anyhow = "1.0.95" +serde = { version = "1.0.216", features = ["derive"] } +base64 = "0.22.1" +serde_json = "1.0.134" \ No newline at end of file diff --git a/bot/src/bluesky.rs b/bot/src/bluesky.rs new file mode 100644 index 0000000..40d3ff5 --- /dev/null +++ b/bot/src/bluesky.rs @@ -0,0 +1 @@ +mod token; diff --git a/bot/src/bluesky/token.rs b/bot/src/bluesky/token.rs new file mode 100644 index 0000000..ce711d1 --- /dev/null +++ b/bot/src/bluesky/token.rs @@ -0,0 +1,131 @@ +use anyhow::anyhow; +use base64::prelude::*; +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; +use std::time::{SystemTime, UNIX_EPOCH}; + +/// Represents the token's internal payload. +#[derive(Serialize, Deserialize)] +struct TokenPayloadInternal { + sub: String, + iat: u64, + exp: u64, + aud: String, +} + +/// Token represents a bluesky authentication token. +#[derive(Serialize, Deserialize, Debug, PartialOrd, PartialEq, Default)] +struct Token { + handle: String, + #[serde(rename(serialize = "accessJwt", deserialize = "accessJwt"))] + access_jwt: String, + #[serde(rename(serialize = "refreshJwt", deserialize = "refreshJwt"))] + refresh_jwt: String, +} + +impl Token { + /// Returns true if the token is expired, false otherwise. + fn is_expired(&self) -> Result { + let parts: Vec<&str> = self.access_jwt.split('.').collect(); + let payload_part = parts.get(1).ok_or(anyhow!("Missing payload from token"))?; + + let result = BASE64_STANDARD_NO_PAD.decode(payload_part)?; + let payload: TokenPayloadInternal = serde_json::from_slice(&result)?; + let now = SystemTime::now(); + let unix_epoch_seconds = now.duration_since(UNIX_EPOCH)?.as_secs(); + Ok(unix_epoch_seconds - 60 >= payload.exp) + } +} + +impl Display for Token { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Token [Handle: {}, AccessJWT: {}, RefreshJWT: {}]", + self.handle, + self.access_jwt.get(0..5).unwrap_or(&self.access_jwt), + self.refresh_jwt.get(0..5).unwrap_or(&self.refresh_jwt), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use anyhow; + + #[test] + fn test_is_expired_true() -> Result<(), anyhow::Error> { + // Setup + let payload = TokenPayloadInternal { + sub: "".to_string(), + iat: 0, + exp: 0, + aud: "".to_string(), + }; + let json_data = serde_json::to_string(&payload)?; + let base64_data = BASE64_STANDARD_NO_PAD.encode(json_data); + + let mut token = Token::default(); + token.access_jwt = format!("eyJ0eXAiOiJhdCtqd3QiLCJhbGciOiJFUzI1NksifQ.{}.oWhKfhGWv6omS3oFQ21GX29uzsd5WrfPJyotJMCQ8V44GF1UN2et7sf_JKVB5jkSuJa6kVWERGuKVGgj8AWScA", base64_data); + + // Test + let result = token.is_expired()?; + + // Assert + assert_eq!(result, true); + Ok(()) + } + + #[test] + fn test_is_expired_false() -> Result<(), anyhow::Error> { + // Setup + let now = SystemTime::now(); + let unix_epoch_seconds = now.duration_since(UNIX_EPOCH)?.as_secs() + 100_000; + let payload = TokenPayloadInternal { + sub: "".to_string(), + iat: 0, + exp: unix_epoch_seconds, + aud: "".to_string(), + }; + let json_data = serde_json::to_string(&payload)?; + let base64_data = BASE64_STANDARD_NO_PAD.encode(json_data); + + let mut token = Token::default(); + token.access_jwt = format!("eyJ0eXAiOiJhdCtqd3QiLCJhbGciOiJFUzI1NksifQ.{}.oWhKfhGWv6omS3oFQ21GX29uzsd5WrfPJyotJMCQ8V44GF1UN2et7sf_JKVB5jkSuJa6kVWERGuKVGgj8AWScA", base64_data); + + // Test + let result = token.is_expired()?; + + // Assert + assert_eq!(result, false); + Ok(()) + } + + #[test] + fn test_token_deserialize() -> Result<(), anyhow::Error> { + let data = r#" + { + "handle": "cool-bot.bsky.social", + "email": "cool@gmail.com", + "emailConfirmed": true, + "emailAuthFactor": false, + "accessJwt": "ein.zwei.drei", + "refreshJwt": "fier.funf.sechs", + "active": true + } +"#; + + let token: Token = serde_json::from_str(data)?; + + assert_eq!( + token, + Token { + handle: "cool-bot.bsky.social".to_string(), + access_jwt: "ein.zwei.drei".to_string(), + refresh_jwt: "fier.funf.sechs".to_string(), + } + ); + Ok(()) + } +} diff --git a/bot/src/main.rs b/bot/src/main.rs index b241c81..177d982 100644 --- a/bot/src/main.rs +++ b/bot/src/main.rs @@ -9,6 +9,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::thread; +mod bluesky; mod cli; //noinspection DuplicatedCode