use std::collections::HashMap;
use std::fmt;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use thiserror::Error;
pub struct RedactedString(String);
impl RedactedString {
pub fn new(s: impl Into<String>) -> Self {
Self(s.into())
}
pub fn expose(&self) -> &str {
&self.0
}
}
impl fmt::Debug for RedactedString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("RedactedString(<redacted>)")
}
}
impl fmt::Display for RedactedString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("<redacted>")
}
}
pub type SecretBundle = HashMap<String, RedactedString>;
#[derive(Error, Debug)]
pub enum BrokerError {
#[error("broker not configured for this server")]
NotConfigured,
#[error("lookup failed for caller: {0}")]
Lookup(String),
#[error("internal broker error: {0}")]
Internal(String),
#[error("bearer expired")]
Expired,
#[error("bearer revoked: {0}")]
Revoked(String),
}
pub type ResolveFuture<'a> =
Pin<Box<dyn Future<Output = Result<Option<Arc<SecretBundle>>, BrokerError>> + Send + 'a>>;
pub type ResolveBearerFuture<'a> =
Pin<Box<dyn Future<Output = Result<Option<BearerIdentity>, BrokerError>> + Send + 'a>>;
#[derive(Debug, Clone)]
pub struct BearerIdentity {
pub caller_id: String,
pub granted_capabilities: Vec<String>,
pub secrets: Option<Arc<SecretBundle>>,
pub expires_at: Option<std::time::SystemTime>,
pub cache_until: Option<std::time::SystemTime>,
}
pub trait TokenBroker: Send + Sync {
fn resolve<'a>(&'a self, caller_id: Option<&'a str>) -> ResolveFuture<'a>;
fn resolve_bearer<'a>(&'a self, _bearer: &'a str) -> ResolveBearerFuture<'a> {
Box::pin(async move { Err(BrokerError::NotConfigured) })
}
fn accepted_token_formats(&self) -> &'static [&'static str] {
&[]
}
}
#[derive(Default)]
pub struct InMemoryTokenBroker {
bundles: HashMap<String, Arc<SecretBundle>>,
ucan_audiences: HashMap<String, String>,
ucan_max_chain_depth: u8,
ucan_revocation_store: Option<Arc<dyn crate::ucan::UcanRevocationStore>>,
}
impl InMemoryTokenBroker {
pub fn new() -> Self {
Self {
bundles: HashMap::new(),
ucan_audiences: HashMap::new(),
ucan_max_chain_depth: 5,
ucan_revocation_store: None,
}
}
pub fn insert(&mut self, caller_id: impl Into<String>, bundle: SecretBundle) {
self.bundles.insert(caller_id.into(), Arc::new(bundle));
}
pub fn register_ucan_audience(
&mut self,
did_key: impl Into<String>,
caller_id: impl Into<String>,
) {
self.ucan_audiences.insert(did_key.into(), caller_id.into());
}
pub fn with_max_chain_depth(mut self, depth: u8) -> Self {
self.ucan_max_chain_depth = depth;
self
}
pub fn with_revocation_store(
mut self,
store: Arc<dyn crate::ucan::UcanRevocationStore>,
) -> Self {
self.ucan_revocation_store = Some(store);
self
}
}
fn looks_like_jwt(s: &str) -> bool {
let parts: Vec<&str> = s.split('.').collect();
if parts.len() != 3 {
return false;
}
parts.iter().all(|seg| {
!seg.is_empty()
&& seg
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
})
}
impl TokenBroker for InMemoryTokenBroker {
fn resolve<'a>(&'a self, caller_id: Option<&'a str>) -> ResolveFuture<'a> {
Box::pin(async move {
let Some(id) = caller_id else {
return Ok(None);
};
Ok(self.bundles.get(id).cloned())
})
}
fn accepted_token_formats(&self) -> &'static [&'static str] {
&["ucan-jwt", "opaque"]
}
fn resolve_bearer<'a>(&'a self, bearer: &'a str) -> ResolveBearerFuture<'a> {
Box::pin(async move {
if !looks_like_jwt(bearer) {
return Err(BrokerError::NotConfigured);
}
let leaf_payload = match crate::ucan::parse_jwt(bearer) {
Ok(p) => p,
Err(_) => return Ok(None),
};
let Some(caller_id) = self.ucan_audiences.get(&leaf_payload.aud).cloned() else {
return Ok(None);
};
let mut cfg = crate::ucan::VerifyConfig::new(leaf_payload.aud.clone());
cfg.max_chain_depth = self.ucan_max_chain_depth;
cfg.revocation_store = self.ucan_revocation_store.clone();
let caps = match crate::ucan::verify_jwt(bearer, &cfg, std::time::SystemTime::now()) {
Ok(c) => c,
Err(crate::ucan::UcanVerifyError::Expired { .. }) => {
return Err(BrokerError::Expired);
}
Err(crate::ucan::UcanVerifyError::Revoked { cid }) => {
return Err(BrokerError::Revoked(cid));
}
Err(_) => return Ok(None),
};
let secrets = self.bundles.get(&caller_id).cloned();
Ok(Some(BearerIdentity {
caller_id,
granted_capabilities: caps.granted(),
secrets,
expires_at: None,
cache_until: None,
}))
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn redacted_string_debug_does_not_leak() {
let s = RedactedString::new("super-secret-token");
let dbg = format!("{:?}", s);
assert!(!dbg.contains("super-secret-token"), "leaked: {dbg}");
assert!(dbg.contains("redacted"));
}
#[test]
fn redacted_string_display_does_not_leak() {
let s = RedactedString::new("super-secret-token");
let disp = format!("{}", s);
assert!(!disp.contains("super-secret-token"), "leaked: {disp}");
assert_eq!(disp, "<redacted>");
}
#[test]
fn redacted_string_expose_returns_value() {
let s = RedactedString::new("super-secret-token");
assert_eq!(s.expose(), "super-secret-token");
}
#[test]
fn secret_bundle_debug_does_not_leak_values() {
let mut bundle = SecretBundle::new();
bundle.insert(
"oauth_token".to_string(),
RedactedString::new("plaintext-token-value"),
);
let dbg = format!("{:?}", bundle);
assert!(!dbg.contains("plaintext-token-value"), "leaked: {dbg}");
assert!(dbg.contains("oauth_token"));
}
#[tokio::test]
async fn in_memory_broker_resolves_known_caller() {
let mut broker = InMemoryTokenBroker::new();
let mut bundle = SecretBundle::new();
bundle.insert("oauth".into(), RedactedString::new("tok-A"));
broker.insert("agent-A", bundle);
let resolved = broker.resolve(Some("agent-A")).await.unwrap();
let bundle = resolved.expect("bundle present");
assert_eq!(bundle.get("oauth").unwrap().expose(), "tok-A");
}
#[tokio::test]
async fn in_memory_broker_returns_none_for_unknown_caller() {
let broker = InMemoryTokenBroker::new();
assert!(broker.resolve(Some("unknown")).await.unwrap().is_none());
}
#[tokio::test]
async fn in_memory_broker_returns_none_for_anonymous_caller() {
let mut broker = InMemoryTokenBroker::new();
broker.insert("agent-A", SecretBundle::new());
assert!(broker.resolve(None).await.unwrap().is_none());
}
#[tokio::test]
async fn non_jwt_bearer_returns_not_configured() {
let broker = InMemoryTokenBroker::new();
let err = broker.resolve_bearer("ce_0123456789abcdef").await;
assert!(matches!(err, Err(BrokerError::NotConfigured)));
}
#[test]
fn default_accepted_token_formats_lists_ucan_jwt_and_opaque() {
let broker = InMemoryTokenBroker::new();
let formats = broker.accepted_token_formats();
assert!(formats.contains(&"ucan-jwt"));
assert!(formats.contains(&"opaque"));
}
mod ucan_jwt_branch {
use super::*;
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use ed25519_dalek::{Signer, SigningKey};
use serde_json::json;
use std::time::UNIX_EPOCH;
fn signing_key_for_seed(seed: u8) -> SigningKey {
let mut bytes = [0u8; 32];
bytes[0] = seed;
SigningKey::from_bytes(&bytes)
}
fn did_key_for(sk: &SigningKey) -> String {
let raw = sk.verifying_key().to_bytes();
let mut prefixed = Vec::with_capacity(34);
prefixed.extend_from_slice(&[0xed, 0x01]);
prefixed.extend_from_slice(&raw);
let mb = multibase::encode(multibase::Base::Base58Btc, &prefixed);
format!("did:key:{mb}")
}
fn build_jwt(payload: serde_json::Value, sk: &SigningKey) -> String {
let header = json!({"alg": "EdDSA", "typ": "ucan/1.0+jwt", "ucv": "1.0"});
let h = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&header).unwrap());
let p = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&payload).unwrap());
let signed = format!("{h}.{p}");
let sig = sk.sign(signed.as_bytes());
let s = URL_SAFE_NO_PAD.encode(sig.to_bytes());
format!("{h}.{p}.{s}")
}
fn future_exp() -> i64 {
(std::time::SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
+ 3600) as i64
}
fn past_exp() -> i64 {
(std::time::SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
- 3600) as i64
}
fn payload_with(
iss: &str,
aud: &str,
caps: &[&str],
prf: &[String],
exp: i64,
) -> serde_json::Value {
json!({
"iss": iss,
"aud": aud,
"sub": iss,
"cmd": "atd-cap",
"args": { "caps": caps, "with": [] },
"nonce": "test-nonce-fixed",
"exp": exp,
"prf": prf
})
}
#[tokio::test]
async fn resolve_bearer_ucan_jwt_returns_identity_from_registered_audience() {
let sk_user = signing_key_for_seed(1);
let sk_agent = signing_key_for_seed(2);
let agent_did = did_key_for(&sk_agent);
let p = payload_with(
&did_key_for(&sk_user),
&agent_did,
&["records:read"],
&[],
future_exp(),
);
let jwt = build_jwt(p, &sk_user);
let mut broker = InMemoryTokenBroker::new();
broker.register_ucan_audience(&agent_did, "agent:A:hk-9001");
let identity = broker.resolve_bearer(&jwt).await.unwrap().unwrap();
assert_eq!(identity.caller_id, "agent:A:hk-9001");
assert_eq!(identity.granted_capabilities, vec!["records:read"]);
}
#[tokio::test]
async fn resolve_bearer_ucan_jwt_unregistered_aud_rejects_with_lookup() {
let sk_user = signing_key_for_seed(1);
let sk_agent = signing_key_for_seed(2);
let p = payload_with(
&did_key_for(&sk_user),
&did_key_for(&sk_agent),
&["records:read"],
&[],
future_exp(),
);
let jwt = build_jwt(p, &sk_user);
let broker = InMemoryTokenBroker::new(); let r = broker.resolve_bearer(&jwt).await;
assert!(
matches!(r, Ok(None)),
"expected Ok(None) for unregistered audience, got {r:?}"
);
}
#[tokio::test]
async fn resolve_bearer_ucan_jwt_expired_returns_expired() {
let sk_user = signing_key_for_seed(1);
let sk_agent = signing_key_for_seed(2);
let agent_did = did_key_for(&sk_agent);
let p = payload_with(
&did_key_for(&sk_user),
&agent_did,
&["records:read"],
&[],
past_exp(), );
let jwt = build_jwt(p, &sk_user);
let mut broker = InMemoryTokenBroker::new();
broker.register_ucan_audience(&agent_did, "agent:A");
let r = broker.resolve_bearer(&jwt).await;
assert!(
matches!(r, Err(BrokerError::Expired)),
"expected BrokerError::Expired, got {r:?}"
);
}
#[tokio::test]
async fn resolve_bearer_ucan_jwt_bad_signature_returns_ok_none() {
let sk_user = signing_key_for_seed(1);
let sk_agent = signing_key_for_seed(2);
let sk_x = signing_key_for_seed(99);
let agent_did = did_key_for(&sk_agent);
let p = payload_with(
&did_key_for(&sk_user),
&agent_did,
&["records:read"],
&[],
future_exp(),
);
let jwt = build_jwt(p, &sk_x);
let mut broker = InMemoryTokenBroker::new();
broker.register_ucan_audience(&agent_did, "agent:A");
let r = broker.resolve_bearer(&jwt).await;
assert!(
matches!(r, Ok(None)),
"expected Ok(None) for bad signature, got {r:?}"
);
}
#[tokio::test]
async fn resolve_bearer_ucan_jwt_secrets_attach_when_caller_has_bundle() {
let sk_user = signing_key_for_seed(1);
let sk_agent = signing_key_for_seed(2);
let agent_did = did_key_for(&sk_agent);
let p = payload_with(
&did_key_for(&sk_user),
&agent_did,
&["records:read"],
&[],
future_exp(),
);
let jwt = build_jwt(p, &sk_user);
let mut broker = InMemoryTokenBroker::new();
broker.register_ucan_audience(&agent_did, "agent:A");
let mut bundle = SecretBundle::new();
bundle.insert("hms_oauth".into(), RedactedString::new("tok-A"));
broker.insert("agent:A", bundle);
let identity = broker.resolve_bearer(&jwt).await.unwrap().unwrap();
let secrets = identity.secrets.expect("bundle should be attached");
assert_eq!(secrets.get("hms_oauth").unwrap().expose(), "tok-A");
}
#[test]
fn looks_like_jwt_accepts_three_base64url_segments() {
assert!(super::super::looks_like_jwt("aaa.bbb.ccc"));
assert!(super::super::looks_like_jwt("a-b_c.dd-ee_.ffG"));
assert!(!super::super::looks_like_jwt("only.two"));
assert!(!super::super::looks_like_jwt("a.b.c.d"));
assert!(!super::super::looks_like_jwt(".b.c")); assert!(!super::super::looks_like_jwt("a.b.c$x")); assert!(!super::super::looks_like_jwt("ce_0123456789abcdef")); }
}
}