#![doc(
issue_tracker_base_url = "https://github.com/mycelial/snowflake-rs/issues",
test(no_crate_inject)
)]
#![doc = include_str ! ("../README.md")]
use base64::Engine;
use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
use openssl::rsa::Rsa;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use thiserror::Error;
use time::{Duration, OffsetDateTime};
use crate::JwtError::JwtEncodingError;
#[derive(Error, Debug)]
pub enum JwtError {
#[error(transparent)]
OpenSslError(#[from] openssl::error::ErrorStack),
#[error("unable to encode JWT: `{0}`")]
JwtEncodingError(String),
}
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
iss: String,
sub: String,
#[serde(with = "jwt_numeric_date")]
iat: OffsetDateTime,
#[serde(with = "jwt_numeric_date")]
exp: OffsetDateTime,
}
impl Claims {
pub fn new(iss: String, sub: String, iat: OffsetDateTime, exp: OffsetDateTime) -> Self {
let iat = iat
.date()
.with_hms_milli(iat.hour(), iat.minute(), iat.second(), 0)
.unwrap()
.assume_utc();
let exp = exp
.date()
.with_hms_milli(exp.hour(), exp.minute(), exp.second(), 0)
.unwrap()
.assume_utc();
Self { iss, sub, iat, exp }
}
}
mod jwt_numeric_date {
use serde::{self, Deserialize, Deserializer, Serializer};
use time::OffsetDateTime;
pub fn serialize<S>(date: &OffsetDateTime, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let timestamp = date.unix_timestamp();
serializer.serialize_i64(timestamp)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<OffsetDateTime, D::Error>
where
D: Deserializer<'de>,
{
OffsetDateTime::from_unix_timestamp(i64::deserialize(deserializer)?)
.map_err(|_| serde::de::Error::custom("invalid Unix timestamp value"))
}
}
fn pubkey_fingerprint(pubkey: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(pubkey);
base64::engine::general_purpose::STANDARD.encode(hasher.finalize())
}
pub fn generate_jwt_token<T: AsRef<[u8]>>(
private_key_pem: T,
full_identifier: &str,
) -> Result<String, JwtError> {
let privk = Rsa::private_key_from_pem(private_key_pem.as_ref())?;
let pubk = privk.public_key_to_der()?;
let iss = format!("{}.SHA256:{}", full_identifier, pubkey_fingerprint(&pubk));
let iat = OffsetDateTime::now_utc();
let exp = iat + Duration::days(1);
let claims = Claims::new(iss, full_identifier.to_owned(), iat, exp);
let ek = EncodingKey::from_rsa_der(&privk.private_key_to_der()?);
encode(&Header::new(Algorithm::RS256), &claims, &ek)
.map_err(|e| JwtEncodingError(e.to_string()))
}