use core::fmt;
use core::str::FromStr;
use chrono::{DateTime, Utc};
use indexmap::IndexMap;
use serde::de::Error as _;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use crate::did::OneOrMany;
use crate::jose::{CompactJws, JwsError, JwsHeader, Signer, encode_compact};
#[derive(Clone, PartialEq, Eq, Hash, Serialize)]
#[serde(transparent)]
pub struct Jwt(String);
impl Jwt {
pub fn new(token: impl Into<String>) -> Result<Self, JwsError> {
let token = token.into();
CompactJws::decode(&token)?;
Ok(Self(token))
}
pub async fn sign(
header: &JwsHeader,
claims: &JwtClaims,
signer: &dyn Signer,
) -> Result<Self, JwsError> {
if let Some(name) = claims.registered_in_additional() {
return Err(JwsError::InvalidPayload(format!(
"registered claim `{name}` is set under `additional`; set its field instead"
)));
}
let payload = serde_json::to_vec(claims).expect("a claims set serializes");
encode_compact(header, &payload, signer).await.map(Self)
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_string(self) -> String {
self.0
}
pub fn decode(&self) -> Result<CompactJws, JwsError> {
CompactJws::decode(&self.0)
}
}
impl fmt::Debug for Jwt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("Jwt").field(&self.0).finish()
}
}
impl fmt::Display for Jwt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for Jwt {
fn as_ref(&self) -> &str {
&self.0
}
}
impl FromStr for Jwt {
type Err = JwsError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
impl TryFrom<String> for Jwt {
type Error = JwsError;
fn try_from(token: String) -> Result<Self, Self::Error> {
Self::new(token)
}
}
impl<'de> Deserialize<'de> for Jwt {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Self::new(String::deserialize(deserializer)?).map_err(D::Error::custom)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct JwtClaims {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub iss: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sub: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub aud: Option<OneOrMany<String>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "numeric_date"
)]
pub exp: Option<i64>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "numeric_date"
)]
pub nbf: Option<i64>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "numeric_date"
)]
pub iat: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub jti: Option<String>,
#[serde(flatten)]
pub additional: IndexMap<String, Value>,
}
pub(crate) fn numeric_date<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Option<i64>, D::Error> {
let Some(number) = Option::<serde_json::Number>::deserialize(deserializer)? else {
return Ok(None);
};
number
.as_i64()
.or_else(|| {
number
.as_f64()
.filter(|seconds| (i64::MIN as f64..i64::MAX as f64).contains(seconds))
.map(|seconds| seconds.floor() as i64)
})
.map(Some)
.ok_or_else(|| D::Error::custom(format!("NumericDate {number} is out of range")))
}
#[cfg(feature = "sd-jwt")]
pub(crate) fn required_numeric_date<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<i64, D::Error> {
numeric_date(deserializer)?.ok_or_else(|| serde::de::Error::custom("a NumericDate is required"))
}
impl JwtClaims {
pub const REGISTERED: [&'static str; 7] = ["iss", "sub", "aud", "exp", "nbf", "iat", "jti"];
pub fn new() -> Self {
Self::default()
}
fn registered_in_additional(&self) -> Option<&str> {
Self::REGISTERED
.into_iter()
.find(|name| self.additional.contains_key(*name))
}
#[must_use]
pub fn with_iss(mut self, issuer: impl Into<String>) -> Self {
self.iss = Some(issuer.into());
self
}
#[must_use]
pub fn with_sub(mut self, subject: impl Into<String>) -> Self {
self.sub = Some(subject.into());
self
}
#[must_use]
pub fn with_jti(mut self, id: impl Into<String>) -> Self {
self.jti = Some(id.into());
self
}
pub fn numeric_date(time: DateTime<Utc>) -> i64 {
time.timestamp()
}
pub fn timestamp(numeric_date: i64) -> Option<DateTime<Utc>> {
DateTime::from_timestamp(numeric_date, 0)
}
pub fn audiences(&self) -> impl Iterator<Item = &str> {
self.aud
.iter()
.flat_map(OneOrMany::iter)
.map(String::as_str)
}
#[must_use]
pub fn with_exp(mut self, time: DateTime<Utc>) -> Self {
self.exp = Some(Self::numeric_date(time));
self
}
#[must_use]
pub fn with_nbf(mut self, time: DateTime<Utc>) -> Self {
self.nbf = Some(Self::numeric_date(time));
self
}
#[must_use]
pub fn with_iat(mut self, time: DateTime<Utc>) -> Self {
self.iat = Some(Self::numeric_date(time));
self
}
#[must_use]
pub fn with_aud(mut self, audience: impl Into<String>) -> Self {
self.aud = Some(OneOrMany::One(audience.into()));
self
}
#[must_use]
pub fn with_claim(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
self.additional.insert(name.into(), value.into());
self
}
}