implement JWT token
This commit is contained in:
parent
291cc55c4d
commit
5c86d4e899
4 changed files with 136 additions and 0 deletions
|
@ -12,3 +12,6 @@ signal-hook = "0.3.17"
|
||||||
log = "0.4.22"
|
log = "0.4.22"
|
||||||
env_logger = "0.11.6"
|
env_logger = "0.11.6"
|
||||||
anyhow = "1.0.95"
|
anyhow = "1.0.95"
|
||||||
|
serde = { version = "1.0.216", features = ["derive"] }
|
||||||
|
base64 = "0.22.1"
|
||||||
|
serde_json = "1.0.134"
|
1
bot/src/bluesky.rs
Normal file
1
bot/src/bluesky.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
mod token;
|
131
bot/src/bluesky/token.rs
Normal file
131
bot/src/bluesky/token.rs
Normal file
|
@ -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<bool, anyhow::Error> {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
|
mod bluesky;
|
||||||
mod cli;
|
mod cli;
|
||||||
|
|
||||||
//noinspection DuplicatedCode
|
//noinspection DuplicatedCode
|
||||||
|
|
Loading…
Reference in a new issue