Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

Open Agent Auth is an enterprise-grade authorization framework that provides cryptographic identity binding, fine-grained authorization, request-level isolation, and semantic audit trails for AI agents operating on behalf of users. **It builds a collaborative ecosystem where humans, agents, and resource providers work as equal partners with mutual trust and accountability.**

The framework is implemented based on [IETF Draft: Agent Operation Authorization (draft-liu-agent-operation-authorization-00)](https://github.com/maxpassion/IETF-Agent-Operation-Authorization-draft/blob/main/draft-liu-agent-operation-authorization-00.xml), extending upon this standard by leveraging industry-standard protocols (OAuth 2.0, OpenID Connect, WIMSE, W3C VC) and featuring Model Context Protocol (MCP) integration to ensure every agent-executed operation is traceable to explicit user consent.
The framework is implemented based on [IETF Draft: Agent Operation Authorization (draft-liu-agent-operation-authorization-01)](https://datatracker.ietf.org/doc/draft-liu-agent-operation-authorization/), extending upon this standard by leveraging industry-standard protocols (OAuth 2.0, OpenID Connect, WIMSE, W3C VC) and featuring Model Context Protocol (MCP) integration to ensure every agent-executed operation is traceable to explicit user consent.

### Project Status

Expand Down Expand Up @@ -326,7 +326,7 @@ For detailed security architecture, see [Security Documentation](docs/architectu

### Standards

- [Agent Operation Authorization Draft](https://github.com/maxpassion/IETF-Agent-Operation-Authorization-draft/blob/main/draft-liu-agent-operation-authorization-00.xml)
- [Agent Operation Authorization Draft](https://datatracker.ietf.org/doc/draft-liu-agent-operation-authorization/)

---

Expand Down
4 changes: 2 additions & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

Open Agent Auth 是一款企业级授权框架,为代表用户执行操作的 AI 智能体提供密码学身份绑定、细粒度授权、请求级隔离与语义审计追踪能力。**该框架构建了一个协作生态系统,人类、智能体与资源提供方以平等伙伴的身份,在相互信任与问责机制下共同协作**。

本框架基于 [IETF 草案:智能体操作授权(draft-liu-agent-operation-authorization-00)](https://github.com/maxpassion/IETF-Agent-Operation-Authorization-draft/blob/main/draft-liu-agent-operation-authorization-00.xml) 标准实现并对其进行扩展,融合 OAuth 2.0、OpenID Connect、WIMSE、W3C VC 等行业标准协议,并集成模型上下文协议(MCP),确保智能体执行的每一项操作均可追溯至明确的用户授权。
本框架基于 [IETF 草案:智能体操作授权(draft-liu-agent-operation-authorization-01)](https://datatracker.ietf.org/doc/draft-liu-agent-operation-authorization/) 标准实现并对其进行扩展,融合 OAuth 2.0、OpenID Connect、WIMSE、W3C VC 等行业标准协议,并集成模型上下文协议(MCP),确保智能体执行的每一项操作均可追溯至明确的用户授权。

### 项目状态

Expand Down Expand Up @@ -326,7 +326,7 @@ Open Agent Auth 在所有层级实施全面的安全措施:

### 标准规范

- [智能体操作授权草案](https://github.com/maxpassion/IETF-Agent-Operation-Authorization-draft/blob/main/draft-liu-agent-operation-authorization-00.xml)
- [智能体操作授权草案](https://datatracker.ietf.org/doc/draft-liu-agent-operation-authorization/)

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,29 +62,26 @@ public class QwenLlmOperationTextRenderer implements OperationTextRenderer {
private static final Logger logger = LoggerFactory.getLogger(QwenLlmOperationTextRenderer.class);

private static final String SYSTEM_INSTRUCTION = """
You are an authorization consent page assistant. Translate a machine-readable Rego policy \
into a concise, scannable explanation for a non-technical user.
You are an authorization consent page assistant. Convert a Rego policy into a brief, \
plain-language explanation a non-technical user can understand in under 5 seconds.

Output MUST use this exact structure:
Output a short paragraph (1–3 sentences). No headings, labels, bullet points, or \
markdown — just plain sentences.

What this authorizes: [ONE sentence. State the action, the target resource, and any scope \
in a single concise sentence. Do NOT repeat or paraphrase the same information twice.]
Sentence 1: State what the agent will be allowed to do and on which resource. \
Use a verb-first style. Example: "Search and purchase books priced under $50."

Key constraints: [Bullet each REAL constraint from the policy, prefixed with "- ". \
Only list limits that ARE explicitly present (e.g., spending caps, time windows, category \
restrictions, rate limits, geographic bounds). NEVER list the absence of a constraint \
(e.g., do NOT write "No spending cap is set"). If no constraints exist beyond what is \
already stated above, omit this section entirely.]

What this means for you: [ONE sentence. State what the agent can do after approval. \
Only mention token expiration if an explicit expiration time is provided in the input.]
Sentence 2–3 (optional): Only if the policy contains explicit constraints such as \
spending caps, time windows, category restrictions, rate limits, or geographic bounds, \
state them naturally. Omit if no constraints exist.

Rules:
1. Be extremely concise. The user should grasp the meaning within 5 seconds of reading.
2. No greetings, no markdown, no extra commentary.
3. Use plain language. Avoid jargon like "Rego", "OPA", "policy", "predicate".
4. Never repeat information across sections. Each section adds NEW information only.
5. Prefer concrete terms: "search for books" over "perform search operations".
1. Plain language only — no jargon (Rego, OPA, policy, predicate, input).
2. Concrete nouns and verbs — "buy books" not "perform purchase operations".
3. Never state the absence of a constraint ("No spending cap" is forbidden).
4. Never add greetings, markdown formatting, or commentary.
5. Only describe what the Rego policy itself permits or restricts. Do not infer \
or mention information not present in the policy (such as token expiration).
6. Respond in English.
""";

Expand Down Expand Up @@ -167,11 +164,7 @@ private String buildPrompt(OperationTextRenderContext context) {
}
}

if (context.getTokenExpiration() != null) {
prompt.append("\nAuthorization valid until: ").append(context.getTokenExpiration()).append("\n");
}

prompt.append("\nPlease provide the three-part explanation (What this authorizes / Key constraints / What this means for you).");
prompt.append("\nDescribe what this policy permits in plain language.");
return prompt.toString();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ open-agent-auth:
refresh-token-expiry: 2592000
id-token-expiry: 3600
authorization-code-expiry: 600
par-request-expiry: 600
auto-register-clients:
enabled: true
clients:
Expand Down Expand Up @@ -91,7 +92,7 @@ sample:
llm-renderer:
enabled: true
# Qwen model to use. Consistent with sample-agent's default model.
model: qwen3-coder-flash
model: qwen3-coder-plus
# Timeout in seconds for LLM calls
timeout: 120

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,14 @@ void shouldRenderOperationTextSuccessfully() {
.thenAnswer(invocation -> {
AssistantContentSimpleConsumers consumers = invocation.getArgument(2);
// Simulate text callback via mock AssistantContent
simulateTextResponse(consumers, "What this authorizes: The agent can search for books.");
simulateTextResponse(consumers, "Search for programming books.");
return null;
});

OperationTextRenderResult result = renderer.render(context);

assertThat(result).isNotNull();
assertThat(result.getRenderedText()).isEqualTo("What this authorizes: The agent can search for books.");
assertThat(result.getRenderedText()).isEqualTo("Search for programming books.");
assertThat(result.getSemanticExpansionLevel()).isEqualTo(SemanticExpansionLevel.MEDIUM);
}
}
Expand All @@ -112,7 +112,7 @@ void shouldReturnHighExpansionWhenOriginalPromptAbsent() {
anyString(), any(TransportOptions.class), any(AssistantContentSimpleConsumers.class)))
.thenAnswer(invocation -> {
AssistantContentSimpleConsumers consumers = invocation.getArgument(2);
simulateTextResponse(consumers, "What this authorizes: Agent can search.");
simulateTextResponse(consumers, "Search for items.");
return null;
});

Expand Down Expand Up @@ -364,8 +364,8 @@ void shouldIncludeRequestContextInPrompt() {
}

@Test
@DisplayName("Should include token expiration in prompt when available")
void shouldIncludeTokenExpirationInPrompt() {
@DisplayName("Should not include token expiration in prompt (policy-only rendering)")
void shouldNotIncludeTokenExpirationInPrompt() {
QwenLlmOperationTextRenderer renderer = new QwenLlmOperationTextRenderer(TEST_MODEL, TEST_TIMEOUT);

java.time.Instant expiration = java.time.Instant.parse("2026-12-31T23:59:59Z");
Expand All @@ -379,7 +379,7 @@ void shouldIncludeTokenExpirationInPrompt() {
anyString(), any(TransportOptions.class), any(AssistantContentSimpleConsumers.class)))
.thenAnswer(invocation -> {
String prompt = invocation.getArgument(0);
assertThat(prompt).contains("Authorization valid until:");
assertThat(prompt).doesNotContain("Authorization valid until:");

AssistantContentSimpleConsumers consumers = invocation.getArgument(2);
simulateTextResponse(consumers, "Rendered");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,22 @@ public static class OAuth2TokenProperties {
*/
private int authorizationCodeExpiry = 600;

/**
* Pushed Authorization Request (PAR) expiry in seconds.
* <p>
* The lifetime of PAR request URIs issued by the authorization server.
* PAR requests must be used within this time window. In flows involving
* user authentication redirects (e.g., to an external IDP), this value
* should be large enough to accommodate the entire authentication flow.
* </p>
* <p>
* Default value: {@code 600} (10 minutes)
* </p>
*
* @see <a href="https://datatracker.ietf.org/doc/html/rfc9126">RFC 9126 - OAuth 2.0 Pushed Authorization Requests</a>
*/
private int parRequestExpiry = 600;

/**
* Gets the access token expiry in seconds.
*
Expand Down Expand Up @@ -539,6 +555,24 @@ public int getAuthorizationCodeExpiry() {
public void setAuthorizationCodeExpiry(int authorizationCodeExpiry) {
this.authorizationCodeExpiry = authorizationCodeExpiry;
}

/**
* Gets the PAR request expiry in seconds.
*
* @return the PAR request expiry in seconds
*/
public int getParRequestExpiry() {
return parRequestExpiry;
}

/**
* Sets the PAR request expiry in seconds.
*
* @param parRequestExpiry the PAR request expiry in seconds to set
*/
public void setParRequestExpiry(int parRequestExpiry) {
this.parRequestExpiry = parRequestExpiry;
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,9 +233,10 @@ public OAuth2ParRequestValidator parRequestValidator() {

@Bean
@ConditionalOnMissingBean
public OAuth2ParServer parServer(OAuth2ParRequestStore parRequestStore, OAuth2ParRequestValidator parRequestValidator) {
logger.info("Creating OAuth2ParServer bean");
return new DefaultOAuth2ParServer(parRequestStore, parRequestValidator);
public OAuth2ParServer parServer(OAuth2ParRequestStore parRequestStore, OAuth2ParRequestValidator parRequestValidator, OpenAgentAuthProperties properties) {
int parRequestExpiry = properties.getCapabilities().getOAuth2Server().getToken().getParRequestExpiry();
logger.info("Creating OAuth2ParServer bean with PAR request expiry: {} seconds", parRequestExpiry);
return new DefaultOAuth2ParServer(parRequestStore, parRequestValidator, parRequestExpiry);
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,44 @@
transform: translateY(0);
}

.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
box-shadow: none;
pointer-events: none;
}

.btn .btn-spinner {
display: none;
width: 16px;
height: 16px;
border: 2px solid transparent;
border-top-color: currentColor;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}

.btn.is-loading .btn-spinner {
display: inline-block;
}

.btn.is-loading .btn-label {
display: none;
}

.btn.is-loading .btn-loading-text {
display: inline;
}

.btn .btn-loading-text {
display: none;
}

@keyframes spin {
to { transform: rotate(360deg); }
}

.footer {
text-align: center;
margin-top: var(--space-4);
Expand Down Expand Up @@ -746,7 +784,7 @@ <h2><i class="bi bi-shield-lock"></i> Operation Authorization Details</h2>
<!-- Rendered natural-language description (when available) -->
<div th:if="${renderedOperationText != null}" class="rendered-operation-block">
<div class="rendered-operation-text" th:text="${renderedOperationText}">
Purchase items under $50 during the Nov 11 promotion (valid until 23:59)
The agent can search and purchase books priced under $50. This authorization expires at 23:59.
</div>
<div class="rendered-operation-meta" th:if="${semanticExpansionLevel != null}">
<span class="expansion-badge" th:text="'Interpretation: ' + ${semanticExpansionLevel}">Interpretation: medium</span>
Expand Down Expand Up @@ -908,10 +946,14 @@ <h2><i class="bi bi-shield-lock"></i> Operation Authorization Details</h2>

<div class="button-group">
<button type="submit" name="action" value="deny" class="btn btn-deny">
<i class="bi bi-x-lg"></i> Deny
<span class="btn-spinner"></span>
<span class="btn-label"><i class="bi bi-x-lg"></i> Deny</span>
<span class="btn-loading-text">Denying…</span>
</button>
<button type="submit" name="action" value="approve" class="btn btn-approve">
<i class="bi bi-check-lg"></i> Approve
<span class="btn-spinner"></span>
<span class="btn-label"><i class="bi bi-check-lg"></i> Approve</span>
<span class="btn-loading-text">Approving…</span>
</button>
</div>
</form>
Expand All @@ -925,6 +967,26 @@ <h2><i class="bi bi-shield-lock"></i> Operation Authorization Details</h2>
const content = header.nextElementSibling;
content.classList.toggle('show');
}

document.querySelectorAll('form').forEach(function(form) {
form.addEventListener('submit', function(event) {
var clickedButton = event.submitter || document.activeElement;
if (clickedButton && clickedButton.name && clickedButton.value) {
var hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = clickedButton.name;
hiddenInput.value = clickedButton.value;
form.appendChild(hiddenInput);
}
var buttons = form.querySelectorAll('button[type="submit"]');
buttons.forEach(function(button) {
button.disabled = true;
if (button === clickedButton) {
button.classList.add('is-loading');
}
});
});
});
</script>
</body>
</html>
Loading
Loading