Real-Time Risk Signals for AI Agent Authorization
Static auth checks don't react mid-session. Wire SSF risk events to your PDP and block agents the moment something looks wrong.
Join the DZone community and get the full member experience.
Join For FreeHere's a scenario worth thinking about.
A financial reconciliation agent is running. It authenticated correctly; the token is valid; every check at session start passed. Twelve minutes in, it reads 847 invoices in 12 seconds. The baseline rate is 15 per minute.
The authorization system doesn't react. It granted access at the start of the session and hasn't been asked again. The anomaly runs to completion. Someone notices later in the logs.
The access control wasn't misconfigured. It just wasn't designed to respond to what happens during a session — only to what's true at the start. Per-invocation policy evaluation helps, but decisions are only as good as the context they evaluate against. If risk signals can't reach the PDP while the agent is running, you're still making stale decisions.
The Missing Piece
What's needed is an event channel — a way for runtime observations to feed back into authorization context in near-real-time. When something anomalous happens, that fact should reach the PDP before the next tool call, not at the next login.
The OpenID Shared Signals Framework (SSF) is a standard built for exactly this. It handles asynchronous propagation of security events between systems. Originally designed for user sessions — an IdP detecting a credential compromise and notifying downstream apps — it maps directly onto agent authorization.
The core unit is a Security Event Token (SET): a signed JWT carrying an event from a transmitter to a receiver. Your transmitters are things like the agent runtime, your identity provider, and your EDR system. Your receiver is a risk engine that maintains per-agent risk scores and pushes updates to the PDP.
SSF defines two standard event profiles:
- CAEP – session lifecycle events: session revoked, assurance level changed, device compliance changed, token claims changed
- RISC – identity risk: credential compromise, account disabled
For agent-specific behavioral signals, you define custom types under your own namespace.
Setting Up the Stream
The risk engine registers with each transmitter to subscribe to the event types it cares about:
POST /sse/stream
Host: idp.acmecorp.com
{
"delivery": {
"method": "urn:ietf:rfc:8936",
"poll_endpoint": "https://idp.acmecorp.com/sse/poll"
},
"events_requested": [
"https://schemas.openid.net/secevent/caep/event-type/session-revoked",
"https://schemas.openid.net/secevent/caep/event-type/assurance-level-change",
"https://schemas.openid.net/secevent/risc/event-type/credential-compromise",
"urn:acmecorp:secevent:agent/anomalous-behavior",
"urn:acmecorp:secevent:agent:privilege-escalation-attempt"
]
}
The last two are custom agent events. Standard CAEP doesn't cover behavioral anomalies specific to autonomous agents, so you extend it.
What an Agent Event Looks Like
When the agent runtime detects the bulk invoice reads, it publishes a SET to the event bus:
{
"iss": "https://agent-runtime.acmecorp.com",
"jti": "evt-agent-anom-00391",
"iat": 1736949120,
"aud": "https://risk-engine.acmecorp.com",
"sub_id": {
"format": "iss_sub",
"iss": "https://agent-runtime.acmecorp.com",
"sub": "agent:payments-reconciler-v2"
},
"events": {
"urn:acmecorp:secevent:agent/anomalous-behavior": {
"anomaly_type": "unexpected_tool_access",
"details": "Agent accessed 847 invoices in 12 seconds; baseline is 15/min",
"recommended_risk_delta": 45,
"tool_invoked": "invoice-read-api",
"event_timestamp": 1736949115000
}
}
}
The SET is a signed JWT. The risk engine validates the signature against the transmitter's JWKS before acting on it. This matters: an agent can't self-publish events to lower its own risk score.
A few design choices worth copying:
- Include
recommended_risk_delta– the emitter has context on severity that the risk engine doesn't. Let it suggest a delta rather than forcing the risk engine to infer from event type alone. - Put enough context in the event body that the receiver can act without a follow-up API call.
- Use a vendor namespace (
urn:yourorg:secevent:agent/...) for custom types.
Standard CAEP events feed into the same channel. If Alice's MFA token gets revoked while her delegated agent is running, the IdP emits an assurance level change event — and any agent running under her delegation automatically gets constrained on the next authorization check, without the agent knowing or doing anything.
The Risk Engine
The risk engine maintains a per-agent score and pushes updates to the PDP when events arrive:
RISK_DELTAS = {
"urn:acmecorp:secevent:agent/anomalous-behavior": +45,
"urn:acmecorp:secevent:agent/privilege-escalation-attempt": +60,
"https://.../caep/event-type/assurance-level-change": +30,
"https://.../caep/event-type/session-revoked": +100,
"https://.../risc/event-type/credential-compromise": +80,
"https://.../caep/event-type/device-compliance-change": +40,
}
def handle_event(set_payload):
subject = resolve_subject(set_payload["sub_id"])
event_type = list(set_payload["events"].keys())[0]
event_body = set_payload["events"][event_type]
delta = RISK_DELTAS.get(event_type, 0)
# Use emitter-suggested delta when available
if event_type.endswith("anomalous-behavior"):
delta = event_body.get("recommended_risk_delta", delta)
# Only increment on adverse direction
if event_type.endswith("assurance-level-change"):
if event_body.get("change_direction") != "decrease":
return
if event_type.endswith("device-compliance-change"):
if event_body.get("current_status") != "not-compliant":
return
new_score = min(100, current_score(subject) + delta)
update_risk_score(subject, new_score)
# Invalidate cached PDP decisions for this agent
pdp_context_cache.invalidate(subject_id=subject)
pdp_context_cache.update(subject_id=subject, risk_score=new_score)
# Cross revocation threshold — trigger session revocation
if new_score >= 80:
publish_session_revoked_event(subject, reason="risk_threshold_exceeded")
The pdp_context_cache.invalidate() call is the key. The PDP caches risk scores with a short TTL — 30 seconds is reasonable — so it doesn't query the risk engine on every single invocation. When a new event arrives, that cache is immediately invalidated. The next AuthZEN evaluation picks up the updated score, not a stale one.
The Full Sequence
Here's how all of this plays out for the reconciliation agent:
T=0:00 – Session starts. payments-reconciler-v2 is delegated by Alice. risk_score: 0, scope: ["read:invoices", "read:payment_records"].
T=0:05 – First invoice read. PDP evaluates with risk_score: 0. Policy allows it. Agent proceeds normally.
T=1:05 – Runtime detects 847 reads in 12 seconds. Publishes anomalous-behavior SET. Risk engine: new_score = 0 + 45 = 45. PDP cache invalidated.
T=1:07 – Agent calls invoice-read-api again. PDP now evaluates with risk_score: 45. Policy requires risk_score < 30 for invoice reads.
{
"decision": false,
"context": {
"reason_admin": {
"en": "Risk score 45 exceeds threshold 30 for invoice:read."
}
}
}
PEP blocks the call. It also emits an event for the repeated access attempt after denial, adding +20: new_score = 65.
T=1:09 – Score crosses the revocation threshold (60). Risk engine publishes a CAEP session-revoked SET directly to the PDP:
{
"events": {
"https://schemas.openid.net/secevent/caep/event-type/session-revoked": {
"initiating_entity": "policy",
"reason_admin": {
"en": "Agent risk score 65 exceeded revocation threshold 60."
}
}
}
}
T=1:10 – Every subsequent call from this agent:
{
"decision": false,
"context": {
"reason_admin": {
"en": "Session revoked: risk threshold exceeded."
}
}
}
Orchestration layer logs the full event chain, notifies Alice, and suspends the agent pending review.
Anomaly detected to full lockout: about five seconds.
Delegated Agents Inherit Parent Risk
If the reconciliation agent had spawned a sub-agent, that sub-agent's authorization context carries the parent's risk score:
"context": {
"parent_session_id": "sess-9f3b2d1a",
"parent_risk_score": 45
}
The Rego policy for delegated agents checks it:
package acmecorp.agents.delegation
import rego.v1
default allow := false
allow if {
input.subject.properties.agent_type == "delegated"
input.subject.properties.delegation_depth <= 2
input.subject.properties.risk_score < 30
input.context.parent_risk_score < 50
}
deny if {
input.subject.properties.delegation_depth > 2
}
deny if {
input.context.parent_risk_score >= 80
}
When the parent's score updates, the risk engine pushes a context update for all child sessions. The sub-agent gets constrained automatically, without re-authenticating. This prevents the common failure mode where a compromised orchestrator agent's children keep running.
Practical Notes
Cache TTLs matter more than you'd think. Risk score cache at 30 seconds is a reasonable starting point. Session state at 10 seconds. For session revocation events specifically, bypass the cache entirely and push directly to the PDP context store — you want that to be immediate.
Sign everything. If agents can publish unsigned events to the risk engine, they can manipulate their own scores. Every SET should be a signed JWT, validated against the transmitter's registered JWKS.
Log every evaluation. Each AuthZEN call should write a log entry linking the decision to the risk score and session state at that moment:
{
"evaluation_id": "eval-8a3c1f2d",
"timestamp": "2025-01-15T14:31:07Z",
"subject_id": "agent:payments-reconciler-v2",
"action": "read",
"resource_type": "invoice",
"decision": false,
"risk_score_at_eval": 45,
"session_id": "sess-9f3b2d1a"
}
Without this, a revoked session gives you a dead end rather than a traceable incident.
Start with what your IdP already sends. Several major identity providers support SSF. If yours does, you get session revocation, assurance level changes, and credential compromise events essentially for free. Custom agent events add behavioral signals on top. No need to build the whole thing at once.
Opinions expressed by DZone contributors are their own.
Comments