use crate::access_token::{AuthError, Claims, VerifyConfig};
use crate::engine::raw::parse_payload_json;
pub(crate) fn run(
token: &str,
mut claims: Claims,
_cfg: &VerifyConfig,
) -> Result<Claims, AuthError> {
if ulid::Ulid::from_string(&claims.sub).is_err() {
return Err(AuthError::SubFormatInvalid);
}
let payload = parse_payload_json(token)?;
if let Some(value) = payload.get("account_type") {
let s = value.as_str().ok_or(AuthError::AccountTypeInvalid)?;
if !matches!(s, "human" | "ai_agent") {
return Err(AuthError::AccountTypeInvalid);
}
claims.account_type = Some(s.to_string());
}
if let Some(value) = payload.get("caps") {
claims.caps = parse_string_array(value, AuthError::CapsShapeInvalid)?;
}
let admin = match payload.get("admin") {
None => false,
Some(v) => v.as_bool().ok_or(AuthError::AdminBandRejected)?,
};
let active_ppnum_str = payload.get("active_ppnum").and_then(|v| v.as_str());
if admin {
let ppnum = active_ppnum_str.ok_or(AuthError::AdminBandRejected)?;
if !is_in_admin_band(ppnum) {
return Err(AuthError::AdminBandRejected);
}
}
claims.admin = admin;
claims.active_ppnum = active_ppnum_str.map(String::from);
if let Some(value) = payload.get("dlg_depth") {
let depth = value.as_u64().ok_or(AuthError::DlgDepthInvalid)?;
if depth > MAX_DLG_DEPTH {
return Err(AuthError::DlgDepthInvalid);
}
}
if let Some(value) = payload.get("scopes") {
let scopes = parse_string_array(value, AuthError::ScopesShapeInvalid)?;
if scopes.len() > MAX_SCOPES {
return Err(AuthError::ScopesTooLong);
}
claims.scopes = scopes;
}
claims.delegator = payload
.get("delegator")
.and_then(|v| v.as_str())
.map(String::from);
claims.cid = payload
.get("cid")
.and_then(|v| v.as_str())
.map(String::from);
claims.sid = payload
.get("sid")
.and_then(|v| v.as_str())
.map(String::from);
if let Some(obj) = payload.as_object() {
for key in obj.keys() {
if !ALLOWED_CLAIMS.contains(&key.as_str()) {
return Err(AuthError::UnknownClaim(key.clone()));
}
}
}
Ok(claims)
}
const MAX_SCOPES: usize = 256;
const ALLOWED_CLAIMS: &[&str] = &[
"iss",
"sub",
"aud",
"exp",
"iat",
"nbf",
"jti",
"client_id",
"cat",
"account_type",
"admin",
"caps",
"delegator",
"dlg_depth",
"cid",
"sv",
"sid",
"active_ppnum",
"scopes",
];
const MAX_DLG_DEPTH: u64 = 4;
const ADMIN_BAND_START: u16 = 100;
const ADMIN_BAND_END: u16 = 109;
fn is_in_admin_band(active_ppnum: &str) -> bool {
if active_ppnum.len() < 3 {
return false;
}
if !active_ppnum.chars().all(|c| c.is_ascii_digit()) {
return false;
}
match active_ppnum[..3].parse::<u16>() {
Ok(band) => (ADMIN_BAND_START..=ADMIN_BAND_END).contains(&band),
Err(_) => false,
}
}
fn parse_string_array(
value: &serde_json::Value,
on_invalid: AuthError,
) -> Result<Vec<String>, AuthError> {
let array = value.as_array().ok_or(on_invalid.clone())?;
let mut out = Vec::with_capacity(array.len());
for item in array {
let s = item.as_str().ok_or(on_invalid.clone())?;
out.push(s.to_string());
}
Ok(out)
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
fn cfg() -> VerifyConfig {
VerifyConfig::access_token("https://accounts.ppoppo.com", "ppoppo")
}
fn claims_with_sub(sub: &str) -> Claims {
Claims {
iss: "https://accounts.ppoppo.com".to_string(),
sub: sub.to_string(),
exp: 9_999_999_999,
iat: 1_700_000_000,
nbf: None,
jti: "01HABC00000000000000000000".to_string(),
client_id: "ppoppo-internal".to_string(),
account_type: None,
caps: Vec::new(),
scopes: Vec::new(),
admin: false,
active_ppnum: None,
delegator: None,
cid: None,
sid: None,
}
}
fn forge_payload(payload: serde_json::Value) -> String {
use base64::Engine;
let header = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(
serde_json::to_vec(&serde_json::json!({"alg":"EdDSA","typ":"at+jwt","kid":"k"}))
.unwrap(),
);
let body = base64::engine::general_purpose::URL_SAFE_NO_PAD
.encode(serde_json::to_vec(&payload).unwrap());
format!("{header}.{body}.<sig>")
}
fn payload_with_sub(sub: &str) -> serde_json::Value {
serde_json::json!({
"iss": "https://accounts.ppoppo.com",
"sub": sub,
"aud": "ppoppo",
"exp": 9_999_999_999i64,
"iat": 1_700_000_000i64,
"jti": "01HABC00000000000000000000",
"client_id": "ppoppo-internal",
"cat": "access",
})
}
#[test]
fn accepts_valid_ulid_sub() {
let claims = claims_with_sub("01HSAB00000000000000000000");
let token = forge_payload(payload_with_sub("01HSAB00000000000000000000"));
assert!(run(&token, claims, &cfg()).is_ok());
}
#[test]
fn rejects_too_short_sub() {
let claims = claims_with_sub("00000000000"); let token = forge_payload(payload_with_sub("00000000000"));
assert_eq!(
run(&token, claims, &cfg()),
Err(AuthError::SubFormatInvalid),
);
}
#[test]
fn rejects_non_crockford_alphabet() {
let claims = claims_with_sub("I1HSUB00000000000000000000");
let token = forge_payload(payload_with_sub("I1HSUB00000000000000000000"));
assert_eq!(
run(&token, claims, &cfg()),
Err(AuthError::SubFormatInvalid),
);
}
#[test]
fn account_type_populated_when_valid() {
let claims = claims_with_sub("01HSAB00000000000000000000");
let mut payload = payload_with_sub("01HSAB00000000000000000000");
payload["account_type"] = serde_json::json!("human");
let token = forge_payload(payload);
let claims = run(&token, claims, &cfg()).expect("M40 valid");
assert_eq!(claims.account_type.as_deref(), Some("human"));
}
}