use super::funejson::Value; use super::auth_bearer_token::parse_bearer_token; use super::auth_jwt_decode_jwt::check_jwt_secret; use super::auth_jwt_verify_jwt::verify_jwt; use super::time_unix_to_iso::unix_to_iso; /// The latest second time.unix-to-iso can write. const MAX_EXP: i64 = 253402300799; /// A failed check: every field but the reason is `None`. pub fn access_denied(error: &str, message: &str) -> AccessCheck { AccessCheck { ok: false, subject: None, token_version: None, jti: None, expires_at: None, claims: None, error: Some(error.to_string()), message: Some(message.to_string()), } } /// A JSON number with no fraction; `1.0` is whole, as it is in JavaScript. fn whole(value: &Value) -> Option { match value { Value::Int(i) => Some(*i), Value::Float(f) if f.is_finite() && f.fract() == 0.0 && f.abs() < 9.007_199_254_740_992e15 => Some(*f as i64), _ => None, } } /// Who is making this request? The Bearer token from the Authorization /// header, verified, and required to carry the claims issue_access_token /// writes. A missing or bad token is an answer (401), never a panic. /// /// # Panics /// Panics on a secret under 32 bytes or a negative leeway. pub fn read_access_token(authorization: Option<&str>, secret: &[i64], now: i64, leeway_seconds: i64) -> AccessCheck { check_jwt_secret(secret); if leeway_seconds < 0 { panic!("leewaySeconds must be a whole number, 0 or more"); } let token = match parse_bearer_token(authorization) { Some(token) => token, None => return access_denied("missing_token", "the request has no Bearer token"), }; let verified = verify_jwt(&token, secret, now, leeway_seconds); let claims = match (verified.valid, verified.claims) { (true, Some(claims)) => claims, _ => { return access_denied( verified.error.as_deref().unwrap_or("malformed_token"), verified.message.as_deref().unwrap_or("the token is not a well-formed JWT"), ) } }; let sub = match claims.get("sub") { Value::Str(s) if !s.is_empty() => Some(s.clone()), _ => None, }; let jti = match claims.get("jti") { Value::Str(s) if !s.is_empty() => Some(s.clone()), _ => None, }; let ver = whole(claims.get("ver")).filter(|v| *v >= 0); let exp = whole(claims.get("exp")).filter(|e| (0..=MAX_EXP).contains(e)); match (sub, jti, ver, exp) { (Some(subject), Some(jti), Some(ver), Some(exp)) => AccessCheck { ok: true, subject: Some(subject), token_version: Some(ver), jti: Some(jti), expires_at: Some(unix_to_iso(exp)), claims: Some(claims), error: None, message: None, }, _ => access_denied("invalid_claims", "the token lacks the sub, jti, ver or exp this API issues"), } } pub fn access_check_to_value(check: &AccessCheck) -> Value { let text = |v: &Option| v.as_deref().map(Value::str).unwrap_or(Value::Null); Value::obj(vec![ ("ok", Value::Bool(check.ok)), ("subject", text(&check.subject)), ("tokenVersion", check.token_version.map(Value::Int).unwrap_or(Value::Null)), ("jti", text(&check.jti)), ("expiresAt", text(&check.expires_at)), ("claims", check.claims.clone().unwrap_or(Value::Null)), ("error", text(&check.error)), ("message", text(&check.message)), ]) } pub fn fune_vector(args: &[Value]) -> Value { let header = match &args[0] { Value::Str(s) => Some(s.as_str()), _ => None, }; let secret: Vec = match &args[1] { Value::Arr(items) => items .iter() .map(|item| match item { Value::Int(i) => *i, _ => panic!("secret must be a list of integers from 0 to 255"), }) .collect(), _ => panic!("secret must be a list of integers from 0 to 255"), }; let now = match &args[2] { Value::Int(i) => *i, _ => panic!("now must be a whole number of Unix seconds"), }; let leeway = match &args[3] { Value::Int(i) => *i, _ => panic!("leewaySeconds must be a whole number, 0 or more"), }; access_check_to_value(&read_access_token(header, &secret, now, leeway)) }