Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

authz-details-rs: RFC 9396 Authorization Details

Your bank’s payment API has adopted OAuth 2.0 Rich Authorization Requests (RFC 9396). Instead of a flat list of scopes, clients now express what they want to do with structured JSON:

{
  "type": "payment_initiation",
  "actions": ["initiate", "status"],
  "instructedAmount": {
    "currency": "EUR",
    "amount": "123.50"
  },
  "creditorName": "Merchant A",
  "creditorAccount": {
    "iban": "DE02100100109307118603"
  }
}

Your authorization server must now answer a new kind of question: is this a well-formed request for a type the server understands? Did the client re-ask for more than it was granted at the authorization endpoint? May a delegated service act on the user’s behalf, within the intersection of both parties’ permissions?

authz-details-rs provides idiomatic Rust types and evaluation functions for RFC 9396 authorization_details: parsing, JSON Schema validation, named rules with JSONPath field conditions and time windows, subset comparison with explanations, and delegation. It is storage-agnostic, designed for use by both authorization servers and resource servers, and shaped like the other BAC crates: rules are built with builders, policies expose evaluate, evaluate_at and check_access, time is an acls-rs Timestamp, and policies compose with acls-rs RBAC and (optionally) hbac-rs HBAC.

Adding authz-details-rs to your project

[dependencies]
authz-details-rs = "0.2"

Optional features:

FeatureEnables
regex-matchersThe ComparisonOp::Matches regular-expression operator
hbacauthz_details_rs::hbac::HostScopedPolicy: HBAC host/service access composed with rules
[dependencies]
authz-details-rs = { version = "0.2", features = ["hbac"] }

Basic usage

Register the types you understand, parse the authorization_details request parameter, validate it, and enforce a rule:

#![allow(unused)]
fn main() {
use authz_details_rs::prelude::*;
use serde_json::json;

let registry = TypeRegistry::new()
    .with_type(TypeSchema::new(
        "payment_initiation",
        json!({
            "type": "object",
            "required": ["actions", "instructedAmount"],
            "properties": {
                "actions": {"type": "array"},
                "instructedAmount": {
                    "type": "object",
                    "properties": {
                        "amount": {"type": "string"},
                        "currency": {"type": "string"}
                    }
                }
            }
        }),
        "Payment initiation authorization",
    ))
    .unwrap();

let details = AuthorizationDetails::from_json(
    r#"[{"type": "payment_initiation",
         "actions": ["initiate"],
         "instructedAmount": {"currency": "EUR", "amount": "100.00"}}]"#,
)
.unwrap();
registry.validate(&details).unwrap();

let mut policy = AuthzDetailsPolicy::new();
policy
    .add_rule(
        AuthzDetailsRule::builder("eur-up-to-1000")
            .type_id("payment_initiation")
            .field("$.instructedAmount.amount").le(1000)
            .field("$.instructedAmount.currency").eq("EUR")
            .build()
            .unwrap(),
    )
    .unwrap();

let result = policy.evaluate(&details);
assert!(result.is_allowed());
assert_eq!(result.matched_rules, ["eur-up-to-1000"]);
}

Core types

TypeDescription
AuthorizationDetailOne authorization_details entry: a type_id plus a JSON object of members, with accessors for the RFC 9396 common fields (actions(), locations(), datatypes(), identifier(), privileges())
AuthorizationDetailsThe whole request parameter (from_json / to_json); dereferences to a slice
TypeSchemaA type: a JSON Schema (Draft 2020-12), an optional ComparatorConfig, and a description
TypeRegistryRegistered types with compiled schemas: validate, is_subset, explain_subset, intersect
ComparatorConfigField-level rules (numeric, array) for typed subset comparison and delegation
AuthzDetailsRuleA named rule for one type: Conditions (field matchers, time windows) combined with a MatchMode
TemporalAuthzDetailsRuleA rule with a valid_from / valid_until window, like TemporalHbacRule
AuthzDetailsPolicyNamed rules with add_rule / remove_rule / enable_rule / disable_rule and evaluate / evaluate_at
AuthzDetailsEvaluationResultallowed, a Verdict (Allow / Deny / NoRule), matched_rules, not_matched_rules, warnings
ComposedPolicyA policy composed with an acls-rs RbacPolicy

The evaluation workflow

StepAPIWhere it runs
ParseAuthorizationDetails::from_jsonAuthorization server: read the request parameter
ValidateTypeRegistry::validateAuthorization server: accept only well-formed details of registered types
EnforceAuthzDetailsPolicy::evaluate_at, ComposedPolicyAuthorization or resource server: named rules, optionally composed with RBAC/HBAC
NarrowTypeRegistry::explain_subsetToken endpoint: a re-request must be covered by the grant; rejections name the offending field
DelegateTypeRegistry::intersectDelegated flows: the effective grant is the intersection of subject and actor grants

Malformed input fails with its own error type – ValidationError, RuleError, PolicyError, TemporalError, SubsetError, DelegationError, all convertible into AuthzDetailsError. Policy outcomes are values, not errors: a denied request, an uncovered re-request and a disjoint delegation are reported through results, reports and empty lists.

Agentic workflows

AI agents acting on behalf of a user map directly onto the four steps. The user’s consent is validated and becomes the subject grant; the agent’s registered capabilities are the actor grant; compute_intersection yields what the agent may do for this user, and each hop to a sub-agent can only narrow it further. Every tool call then requests a task-scoped token that must pass is_subset, with locations binding the token to one resource server, and a rejection names the field that exceeded the grant. Finally the resource server evaluates its own rules composed with RBAC for the agent role (see the acls-rs bridge) and, on Linux hosts, with HBAC host access (see HBAC Integration), which catches prompt-injected requests that the agent itself did not stop.

Two runnable examples walk through the whole flow, in both Rust (crates/authz-details-rs/examples/) and Python (crates/authz-details-python/examples/):

ExampleScenario
agent_linux_workstationA coding agent on a Linux host: linux_file, linux_exec and systemd_unit details; denies ~/.ssh reads, sudo, other hosts, paths smuggled behind allowed ones, and work outside working hours
agent_web_delegationA travel agent and a payment sub-agent: RFC 9396 payments, calendar and email; denies over-limit amounts, other creditors, token replay at another bank, expired mandates, and exfiltration by email
cargo run -p authz-details-rs --features hbac --example agent_linux_workstation
cargo run -p authz-details-rs --example agent_web_delegation