use serde_json::{json, Value};
use crate::budget::Budget;
use crate::discovery::KeyResolver;
use crate::error::{Error, Result};
use crate::httpsig::{self, Incoming, Verified, VerifyOptions};
use crate::identifier::is_server_identifier;
use crate::keys::SigningKey;
use crate::metadata::dwk;
use crate::requirement::Requirement;
use crate::token::{mint, verify_token, TokenKind, VerifiedToken, VerifyTokenOptions};
pub const DEFAULT_RESOURCE_TOKEN_LIFETIME: i64 = 300;
#[derive(Clone, Debug)]
pub struct Authenticated {
pub signature: Verified,
pub token: VerifiedToken,
}
pub async fn authenticate(
request: &Incoming<'_>,
resource: &str,
accept: &[TokenKind],
options: &VerifyOptions,
resolver: &dyn KeyResolver,
) -> Result<Authenticated> {
let signature = httpsig::verify(request, options, &crate::discovery::NoKeys).await?;
let Some(jwt) = signature.jwt() else {
return Err(crate::SignatureError::new("unsupported_scheme", "sign with sig=jwt").into());
};
let token = verify_token(
&VerifyTokenOptions {
now: options.now,
..VerifyTokenOptions::new(jwt, &signature.thumbprint, resource, accept)
},
resolver,
)
.await?;
Ok(Authenticated { signature, token })
}
#[derive(Clone, Debug)]
pub struct ResourceTokenParams<'a> {
pub resource: &'a str,
pub presented: &'a VerifiedToken,
pub agent_jkt: &'a str,
pub scope: &'a str,
pub budget: Option<&'a Budget>,
pub r3: Option<(&'a str, &'a str)>,
pub mission_expires_at: Option<i64>,
pub now: i64,
pub lifetime: i64,
}
impl<'a> ResourceTokenParams<'a> {
pub fn new(
resource: &'a str,
presented: &'a VerifiedToken,
agent_jkt: &'a str,
scope: &'a str,
) -> Self {
Self {
resource,
presented,
agent_jkt,
scope,
budget: None,
r3: None,
mission_expires_at: None,
now: crate::now_unix(),
lifetime: DEFAULT_RESOURCE_TOKEN_LIFETIME,
}
}
pub fn budget(mut self, budget: &'a Budget) -> Self {
self.budget = Some(budget);
self
}
pub fn r3(mut self, uri: &'a str, s256: &'a str) -> Self {
self.r3 = Some((uri, s256));
self
}
}
pub fn resource_token(key: &SigningKey, params: &ResourceTokenParams<'_>) -> Result<String> {
if !is_server_identifier(params.resource) {
return Err(Error::Invalid(format!(
"{} is not a server identifier",
params.resource
)));
}
if params.scope.is_empty() {
return Err(Error::Invalid("a resource token needs a scope".into()));
}
let presented = params.presented;
let ps = presented.ps().unwrap_or(presented.iss.as_str());
let jti = presented.jti().ok_or_else(|| {
Error::Invalid("the presented token has no jti, so a resource token cannot name it".into())
})?;
let exp = match params.mission_expires_at {
Some(end) => (params.now + params.lifetime).min(end),
None => params.now + params.lifetime,
};
if exp <= params.now {
return Err(Error::Invalid("the mission has already ended".into()));
}
let mut claims = json!({
"iss": params.resource,
"dwk": dwk::RESOURCE,
"aud": ps,
"ps": ps,
"jti": crate::random_id(),
"sub": presented.sub,
"presented_jti": jti,
"person_token_jti": jti,
"agent_jkt": params.agent_jkt,
"scope": params.scope,
"iat": params.now,
"exp": exp,
});
if let Some(mission) = presented.mission_s256() {
claims["mission_s256"] = json!(mission);
}
if let Some(tenant) = presented.tenant() {
claims["tenant"] = json!(tenant);
}
if let Some(budget) = params.budget {
claims["budget"] = serde_json::to_value(budget).unwrap_or(Value::Null);
}
if let Some((uri, s256)) = params.r3 {
claims["r3_uri"] = json!(uri);
claims["r3_s256"] = json!(s256);
}
Ok(mint(key, TokenKind::Resource, &claims))
}
pub fn challenge(requirement: &Requirement) -> Vec<(String, String)> {
vec![(
crate::requirement::HEADER.to_string(),
requirement.to_header(),
)]
}