Skip to content
Clone
b6f7f73
130 lines 4.1 KB Rust
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AppClaims {
#[serde(skip)]
pub is_admin: bool,
#[serde(rename = "iss", alias = "issuer")]
pub issuer: String,
#[serde(rename = "client_id", alias = "app_id")]
pub app_id: String,
#[serde(rename = "sub", alias = "user_id", with = "subject")]
pub user_id: i64,
#[serde(rename = "aud", alias = "audience")]
pub audience: String,
#[serde(rename = "jti", alias = "token_id")]
pub token_id: String,
#[serde(rename = "iat", alias = "issued_at")]
pub issued_at: i64,
#[serde(rename = "exp", alias = "expires_at")]
pub expires_at: i64,
#[serde(default)]
pub security_version: i32,
#[serde(
default,
rename = "sid",
alias = "session_id",
skip_serializing_if = "Option::is_none"
)]
pub session_id: Option<String>,
#[serde(skip)]
pub mfa_verified: bool,
#[serde(default, with = "scopes", skip_serializing_if = "Option::is_none")]
pub scope: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub username: Option<String>,
#[serde(default = "default_token_type")]
pub token_type: String,
}
pub mod subject {
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S: Serializer>(value: &i64, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&value.to_string())
}
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<i64, D::Error> {
#[derive(Deserialize)]
#[serde(untagged)]
enum Subject {
Text(String),
Number(i64),
}
match Subject::deserialize(deserializer)? {
Subject::Text(value) => value.parse().map_err(serde::de::Error::custom),
Subject::Number(value) => Ok(value),
}
}
}
pub(crate) mod scopes {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub fn serialize<S: Serializer>(
value: &Option<Vec<String>>,
serializer: S,
) -> Result<S::Ok, S::Error> {
value
.as_ref()
.map(|scopes| scopes.join(" "))
.serialize(serializer)
}
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Option<Vec<String>>, D::Error> {
#[derive(Deserialize)]
#[serde(untagged)]
enum Scopes {
Text(String),
List(Vec<String>),
}
Ok(
Option::<Scopes>::deserialize(deserializer)?.map(|value| match value {
Scopes::Text(value) => value.split_ascii_whitespace().map(str::to_owned).collect(),
Scopes::List(value) => value,
}),
)
}
}
pub(crate) fn default_token_type() -> String {
"access".to_owned()
}
impl AppClaims {
pub fn is_expired(&self, now: i64, leeway_secs: i64) -> bool {
self.expires_at.saturating_add(leeway_secs.max(0)) <= now
}
pub fn is_service(&self) -> bool {
self.token_type == "service"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn claims_emit_standard_fields_and_preserve_bounded_empty_scope() {
let legacy = serde_json::json!({
"issuer": "fixture", "app_id": "api", "user_id": 42, "audience": "gitdata.ai",
"token_id": "id", "issued_at": 10, "expires_at": 20, "scope": [],
});
let claims: AppClaims = serde_json::from_value(legacy).unwrap();
let standard = serde_json::to_value(&claims).unwrap();
assert_eq!(standard["sub"], "42");
assert_eq!(standard["iss"], "fixture");
assert_eq!(standard["exp"], 20);
assert_eq!(standard["scope"], "");
assert!(standard.get("expires_at").is_none());
let claims: AppClaims = serde_json::from_value(standard).unwrap();
assert!(crate::scope::require(&claims, "guild").is_err());
let mut scoped = claims.clone();
scoped.scope = Some(vec!["profile".into()]);
assert!(crate::scope::http(&scoped, "GET", "/me/profile").is_ok());
assert!(crate::scope::http(&scoped, "GET", "/guilds").is_err());
}
}