Skip to main content

Just-in-Time (JIT) Permissions

JIT permissions shift the security model from "access by default" to "access by request". Instead of granting broad capabilities upfront, agents request a specific permission at the moment they need it, and LumoAuth returns a short-lived token (typically 5–15 minutes) scoped to exactly that operation. High-risk operations can be routed through human approval (HITL) before the token is issued.

Security-first design

JIT permissions are granted with the minimum scope and duration needed. All grants are logged for audit and can be revoked at any time.

Standards and specifications

StandardDescriptionLumoAuth implementation
RFC 9396Rich Authorization Requests (RAR) — lets clients attach structured JSON authorization_details to a token request instead of opaque scope stringsFull support for authorization_details
RFC 8693OAuth 2.0 Token Exchange — swap one token for another, recording subject/actor; used here to downscope a broad agent token into a JIT-scoped oneDownscoping tokens with specific RAR objects
RAR Error Signaling (draft)Insufficient-Authorization-Details response header that tells the client exactly which RAR object is neededUsed by agents to self-correct
CAEPContinuous Access Evaluation Profile (OpenID). A standard for identity providers to push real-time session-event signals (session revoked, credential change) to relying parties so they can invalidate sessions immediately.Real-time token revocation on suspicious behavior

Core concepts

1. Ephemeral personas (task-based identity)

Every agent task gets a unique sub-identity via a task_id. This isolates different workflows, even for the same agent.

{
"sub": "agent:research-bot:task:task_abc123def456",
"agent_id": "agt_research-bot",
"task_id": "task_abc123def456",
"parent_task_id": null,
"jit": true,
"exp": 1706644800, // 10 minutes from now
"authorization_details": [{
"type": "file_access",
"actions": ["read"],
"identifier": "report_2024.pdf"
}]
}

2. RFC 9396 authorization details

Instead of generic scopes like files.read, agents request specific authorization using structured JSON objects (RAR):

{
"type": "file_access",
"actions": ["read"],
"identifier": "report_2024.pdf",
"locations": ["https://storage.example.com/docs/"]
}

Common authorization types:

TypeActionsDescription
file_accessread, write, deleteAccess to specific files or directories
api_callGET, POST, PUT, DELETEHTTP API operations
database_queryselect, insert, update, deleteDatabase operations on specific tables
tool_invocationexecuteInvoking external tools or functions
paymentinitiate, approveFinancial operations (high-risk, requires HITL)
user_dataread, exportAccess to user PII (high-risk)

3. Human-in-the-loop (HITL)

High-risk operations pause for human approval before a token is issued:

4. Token downscoping (RFC 8693)

Agents start with minimal permissions and exchange their broad token for a narrower JIT token scoped to a single action. The JIT token encodes the full authorization_details and has a short TTL.

API reference

Create task (ephemeral persona)

POST /orgs/{orgId}/api/v1/jit/task

Create a new ephemeral task context for the agent.

curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/jit/task \
-H "Authorization: Bearer $AGENT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Research Task #123",
"type": "research",
"on_behalf_of": "alice@example.com"
}'
{
"task_id": "task_a1b2c3d4e5f6g7h8",
"caep_session_id": "caep_xyz789",
"expires_at": "2026-01-30T15:00:00Z",
"agent_id": "agt_research-bot",
"on_behalf_of": "alice@example.com"
}

Request JIT permission

POST /orgs/{orgId}/api/v1/jit/request

Request a specific permission using RFC 9396 authorization_details.

curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/jit/request \
-H "Authorization: Bearer $AGENT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"task_id": "task_a1b2c3d4e5f6g7h8",
"authorization_details": {
"type": "file_access",
"actions": ["read"],
"identifier": "report_2024.pdf"
},
"justification": "Need to analyze Q4 financial data for user query",
"requested_ttl": 300,
"callback_url": "https://agent.example.com/webhook/jit"
}'
{
"request_id": "jit_abc123xyz",
"status": "approved",
"risk_level": "low",
"task_id": "task_a1b2c3d4e5f6g7h8",
"token_url": "/orgs/acme-corp/api/v1/jit/request/jit_abc123xyz/token",
"granted_ttl": 300
}
{
"request_id": "jit_def456uvw",
"status": "pending",
"risk_level": "high",
"task_id": "task_a1b2c3d4e5f6g7h8",
"status_url": "/orgs/acme-corp/api/v1/jit/request/jit_def456uvw/status",
"expires_at": "2026-01-30T14:05:00Z",
"message": "Request requires human approval. Poll status_url or wait for callback."
}

Get request status

GET /orgs/{orgId}/api/v1/jit/request/{request_id}/status

Poll for HITL approval status.

Exchange for a JIT token

POST /orgs/{orgId}/api/v1/jit/request/{request_id}/token

Exchange an approved request for a short-lived JIT token.

{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 300,
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
"authorization_details": [{
"type": "file_access",
"actions": ["read"],
"identifier": "report_2024.pdf"
}],
"task_id": "task_a1b2c3d4e5f6g7h8",
"jit_request_id": "jit_abc123xyz"
}

Complete task

POST /orgs/{orgId}/api/v1/jit/task/{task_id}/complete

Mark the task complete and revoke all associated JIT tokens.

Error signaling (agent self-correction)

When a resource server rejects a request due to insufficient permissions, it returns 403 Forbidden with the Insufficient-Authorization-Details header naming the exact permission required:

HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_authorization_details"
Insufficient-Authorization-Details: eyJ0eXBlIjoiZmlsZV9hY2Nlc3MiLC...

{
"error": "insufficient_authorization_details",
"error_description": "Token lacks required permission",
"authorization_details_hint": {
"type": "file_access",
"actions": ["write"],
"identifier": "report_2024.pdf"
}
}

The agent can automatically parse this response and request the specific permission:

import base64
import json
import requests

def call_api_with_jit(agent_token, task_id, api_url, method="GET"):
"""Call API with automatic JIT permission escalation."""

response = requests.request(method, api_url, headers={
"Authorization": f"Bearer {agent_token}"
})

if response.status_code == 403:
# Check for insufficient_authorization_details header
header = response.headers.get("Insufficient-Authorization-Details")
if header:
# Decode the required authorization_details
required_authz = json.loads(base64.b64decode(header))

# Request JIT permission
jit_response = requests.post(
"https://app.lumoauth.dev/orgs/acme-corp/api/v1/jit/request",
headers={"Authorization": f"Bearer {agent_token}"},
json={
"task_id": task_id,
"authorization_details": required_authz,
"justification": "Required for user request"
}
)

if jit_response.json()["status"] == "approved":
# Get the JIT token
token_url = jit_response.json()["token_url"]
token_response = requests.post(
f"https://app.lumoauth.dev{token_url}",
headers={"Authorization": f"Bearer {agent_token}"}
)
jit_token = token_response.json()["access_token"]

# Retry with JIT token
return requests.request(method, api_url, headers={
"Authorization": f"Bearer {jit_token}"
})

return response

Best practices

PracticeDescriptionImplementation
Ephemeral personasCreate a new sub-identity for every task or threadUse task_id claim in JWT; call /jit/task at workflow start
Token downscopingStart with zero permissions; add only what's needed per tool-callRequest specific authorization_details per operation
Human-in-the-loopFor high-risk JIT requests (delete, payment), pause token issuanceConfigure webhook for approval notifications
Short TTLsJIT tokens should rarely last longer than 5–15 minutesUse requested_ttl (max 900 seconds)
Continuous validationDon't just check at issuance — check during useCAEP evaluates risk continuously and revokes suspicious tokens
Self-correctionAgents should request missing permissions automaticallyParse Insufficient-Authorization-Details header on 403

Risk levels and HITL

JIT requests are assessed for risk based on several factors:

Risk levelTriggersBehavior
LowRead-only operations, non-sensitive resourcesAuto-approved instantly
MediumWrite operations, elevated task risk scoreAuto-approved with audit logging
HighDelete, admin, execute actions; sensitive resource typesRequires human approval (HITL)
CriticalPayment, user_data, credentials; CAEP flags; multiple denialsRequires human approval + extra review

CAEP continuous validation

LumoAuth continuously evaluates agent behavior during task execution:

  • Risk Score Accumulation: Each permission request adds to task risk score
  • Denial Tracking: Multiple denials trigger automatic task suspension
  • Anomaly Detection: High-frequency requests or unusual patterns trigger flags
  • Real-time Revocation: Suspicious tasks have all JIT tokens revoked immediately
{
"task_id": "task_a1b2c3d4e5f6g7h8",
"active": false,
"events": [
{
"type": "risk_threshold_exceeded",
"details": {
"risk_score": 65.0,
"denial_count": 3
},
"timestamp": "2026-01-30T14:25:00Z"
}
],
"action": "suspended"
}

Complete integration example

This example covers the full JIT workflow using the lumoauth Python SDK:

  • Agent authentication using client credentials
  • User consent via OAuth delegation (on-behalf-of flow)
  • Ephemeral task creation for isolated personas
  • JIT permission requests with HITL support
  • Token exchange for scoped, short-lived access

Install

pip install lumoauth

Set the required environment variables:

export LUMOAUTH_URL=https://app.lumoauth.dev
export LUMOAUTH_ORG_ID=acme-corp
export AGENT_CLIENT_ID=agt_...
export AGENT_CLIENT_SECRET=secret_...

Full Example

from lumoauth import LumoAuthAgent
from lumoauth.jit import JITContext

# 1. Authenticate the agent (client credentials)
agent = LumoAuthAgent()
agent.authenticate()

# 2. Create a JIT context (context-manager revokes tokens on exit)
with JITContext(agent) as jit:
# 3. (Optional) Delegate on behalf of a user
# user_token is obtained when the user logs in via OAuth
# jit.delegate_on_behalf_of(user_token)

# 4. Create an ephemeral task
jit.create_task(
name="Analyse Q4 Financial Report",
task_type="analysis",
on_behalf_of="alice@acme-corp.com",
)

# 5. Request a specific permission (RFC 9396)
result = jit.request_permission(
{
"type": "file_access",
"actions": ["read"],
"identifier": "quarterly_report_q4_2024.pdf",
"locations": ["https://storage.acme-corp.com/finance/"],
},
justification="User asked: 'What were our Q4 revenues?'",
)

if result["status"] == "approved":
# 6. Exchange approval for a short-lived JIT token
jit_token = jit.get_token(result["request_id"])

# 7. Use the token to access the protected resource
resp = jit.call(
jit_token,
"GET",
"https://storage.acme-corp.com/finance/quarterly_report_q4_2024.pdf",
)
print(f"Read {len(resp.content)} bytes")

elif result["status"] == "denied":
print(f"Permission denied: {result.get('deny_reason')}")

Auto-escalation

call_with_escalation handles the 403 → JIT request → retry loop when a resource returns an Insufficient-Authorization-Details header:

with JITContext(agent) as jit:
jit.create_task(name="Ad-hoc data access")

resp = jit.call_with_escalation(
"GET",
"https://api.acme-corp.com/v1/documents/doc_9982",
justification="User asked for summary of doc_9982",
)
print(resp.json())
Debugging tip

Check the error field in responses to confirm success. Use the details object for machine-readable error information.

Framework examples

The same patterns work in any HTTP-capable language. Pick your framework:

FrameworkGuide
LangChain / LangGraphView example →
CrewAIView example →
OpenAI Agents SDKView example →
AgnoView example →
Google ADKView example →