diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..4acca5a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,8 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Discussions
+ url: https://github.com/alibaba/open-agent-auth/discussions
+ about: Ask questions and discuss ideas
+ - name: Documentation
+ url: https://github.com/alibaba/open-agent-auth/tree/main/docs
+ about: Read the documentation
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..0fad64c
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,145 @@
+name: CI
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+permissions:
+ contents: write
+ checks: write
+ pull-requests: write
+
+jobs:
+ build:
+ name: Build & Test (JDK ${{ matrix.java }})
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ java: [ '17' ]
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up JDK ${{ matrix.java }}
+ uses: actions/setup-java@v4
+ with:
+ java-version: ${{ matrix.java }}
+ distribution: 'temurin'
+ cache: maven
+
+ - name: Build & Test with JaCoCo
+ id: build
+ run: mvn clean verify -Paggregate-report -B -ntp
+
+ - name: Publish Test Results
+ uses: dorny/test-reporter@v1
+ if: >-
+ always()
+ && (steps.build.outcome == 'success' || steps.build.outcome == 'failure')
+ && github.event_name != 'pull_request'
+ || github.event.pull_request.head.repo.full_name == github.repository
+ with:
+ name: Test Results (JDK ${{ matrix.java }})
+ path: '**/target/surefire-reports/TEST-*.xml'
+ reporter: java-junit
+
+ - name: Generate JaCoCo Coverage Report
+ uses: madrapps/jacoco-report@v1.7.1
+ if: >-
+ always()
+ && (steps.build.outcome == 'success' || steps.build.outcome == 'failure')
+ && github.event_name == 'pull_request'
+ with:
+ paths: '**/target/site/jacoco-aggregate/jacoco.xml'
+ token: ${{ secrets.GITHUB_TOKEN }}
+ title: Code Coverage Report
+ update-comment: true
+ min-coverage-overall: 50
+ min-coverage-changed-files: 60
+
+ - name: Extract coverage percentage
+ if: github.ref == 'refs/heads/main' && github.event_name == 'push'
+ id: coverage
+ run: |
+ REPORT=$(find . -path "*/jacoco-aggregate/jacoco.xml" -type f | head -1)
+ if [ -z "$REPORT" ]; then
+ REPORT=$(find . -path "*/site/jacoco/jacoco.xml" -type f | head -1)
+ fi
+
+ if [ -n "$REPORT" ]; then
+ # Extract the last (root-level) LINE counter from the report
+ # Use awk to reliably parse missed/covered from the last LINE counter element
+ MISSED=$(awk -F'"' '//dev/null && [ "$COVERED" -ge 0 ] 2>/dev/null; then
+ TOTAL=$((MISSED + COVERED))
+ if [ "$TOTAL" -gt 0 ]; then
+ PERCENTAGE=$((COVERED * 100 / TOTAL))
+ else
+ PERCENTAGE=0
+ fi
+ echo "coverage=$PERCENTAGE" >> $GITHUB_OUTPUT
+ echo "Coverage: $PERCENTAGE% ($COVERED/$TOTAL lines)"
+ else
+ echo "coverage=0" >> $GITHUB_OUTPUT
+ echo "Could not parse coverage from $REPORT"
+ fi
+ else
+ echo "coverage=0" >> $GITHUB_OUTPUT
+ echo "No JaCoCo report found"
+ fi
+
+ - name: Update coverage badge in README
+ if: github.ref == 'refs/heads/main' && github.event_name == 'push' && steps.coverage.outputs.coverage != '0'
+ run: |
+ COVERAGE=${{ steps.coverage.outputs.coverage }}
+
+ # Determine badge color based on coverage
+ if [ "$COVERAGE" -ge 80 ]; then
+ COLOR="brightgreen"
+ elif [ "$COVERAGE" -ge 60 ]; then
+ COLOR="green"
+ elif [ "$COVERAGE" -ge 40 ]; then
+ COLOR="yellow"
+ else
+ COLOR="red"
+ fi
+
+ # Update both README.md and README.zh-CN.md
+ for README_FILE in README.md README.zh-CN.md; do
+ if [ -f "$README_FILE" ]; then
+ sed -i "s|!\[Code Coverage\](https://img.shields.io/badge/coverage-[^)]*)||g" "$README_FILE"
+ fi
+ done
+
+ # Check if there are changes to commit
+ if git diff --quiet; then
+ echo "No coverage badge changes to commit"
+ else
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+ git add README.md README.zh-CN.md
+ git commit -m "chore: update coverage badge to ${COVERAGE}% [skip ci]"
+ git push
+ fi
+
+ - name: Upload Test Reports
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: test-reports-jdk${{ matrix.java }}
+ path: '**/target/surefire-reports/'
+ retention-days: 7
+
+ - name: Upload JaCoCo Reports
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: jacoco-reports-jdk${{ matrix.java }}
+ path: '**/target/site/jacoco*/'
+ retention-days: 7
diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml
new file mode 100644
index 0000000..e8f2360
--- /dev/null
+++ b/.github/workflows/dependency-review.yml
@@ -0,0 +1,23 @@
+name: Dependency Review
+
+on:
+ pull_request:
+ branches: [ main ]
+
+permissions:
+ contents: read
+
+jobs:
+ dependency-review:
+ name: Dependency Review
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Dependency Review
+ uses: actions/dependency-review-action@v4
+ with:
+ fail-on-severity: high
+ deny-licenses: GPL-2.0, GPL-3.0, AGPL-3.0
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
new file mode 100644
index 0000000..8f6f252
--- /dev/null
+++ b/.github/workflows/e2e.yml
@@ -0,0 +1,70 @@
+name: E2E Integration Tests
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ e2e-tests:
+ name: E2E Tests (JDK ${{ matrix.java }})
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ strategy:
+ fail-fast: false
+ matrix:
+ java: [ '17' ]
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up JDK ${{ matrix.java }}
+ uses: actions/setup-java@v4
+ with:
+ java-version: ${{ matrix.java }}
+ distribution: 'temurin'
+ cache: maven
+
+ - name: Verify Chrome installation
+ run: |
+ # GitHub Actions ubuntu-latest runners come with Google Chrome pre-installed.
+ # No need to install via apt/snap (which can hang due to snap daemon issues).
+ google-chrome --version || chromium-browser --version
+
+ - name: Build project
+ run: mvn clean install -DskipTests -B -ntp
+
+ - name: Run E2E tests
+ run: |
+ chmod +x open-agent-auth-integration-tests/scripts/run-e2e-tests.sh
+ chmod +x open-agent-auth-samples/scripts/*.sh
+ cd open-agent-auth-integration-tests
+ ./scripts/run-e2e-tests.sh --skip-build
+
+ - name: Upload test reports
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: e2e-test-reports-jdk${{ matrix.java }}
+ path: open-agent-auth-integration-tests/target/surefire-reports/
+ retention-days: 7
+
+ - name: Upload service logs
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: e2e-service-logs-jdk${{ matrix.java }}
+ path: open-agent-auth-samples/logs/
+ retention-days: 7
+
+ - name: Stop services
+ if: always()
+ run: |
+ cd open-agent-auth-samples
+ ./scripts/sample-stop.sh --force || true
diff --git a/README.md b/README.md
index de465fe..bafa08c 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@



- 
+ 

[Quick Start](#quick-start) · [Architecture](#architecture) · [Security](#security) · [Documentation](#documentation) · [Roadmap](#roadmap)
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 4b23f44..1561396 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -10,7 +10,7 @@



- 
+ 

[快速开始](#快速开始) · [架构](#架构) · [安全机制](#安全机制) · [文档资源](#文档资源) · [路线图](#路线图)
diff --git a/docs/architecture/README.md b/docs/architecture/README.md
index ffdb11f..93f432d 100644
--- a/docs/architecture/README.md
+++ b/docs/architecture/README.md
@@ -164,5 +164,155 @@ open-agent-auth:
---
+## Peers Configuration (Convention over Configuration)
+
+### Overview
+
+The **peers** configuration provides a simplified way to declare peer services in the trust domain. Instead of separately configuring JWKS consumers, service discovery entries, and key definitions, a single `peers` declaration automatically expands into all required infrastructure.
+
+### How It Works
+
+When you declare a peer:
+
+```yaml
+open-agent-auth:
+ peers:
+ agent-idp:
+ issuer: http://localhost:8082
+```
+
+The `RoleAwareConfigurationPostProcessor` automatically:
+
+1. **Creates a JWKS consumer** for the peer (equivalent to `infrastructures.jwks.consumers.agent-idp`)
+2. **Creates a service discovery entry** for the peer (equivalent to `infrastructures.service-discovery.services.agent-idp`)
+3. **Infers key definitions** based on the enabled role's profile (e.g., an `authorization-server` role automatically gets `aoat-signing`, `jwe-decryption`, and `wit-verification` keys)
+4. **Ensures a default key provider** exists (in-memory) if none is configured
+5. **Enables the JWKS provider** if the role profile requires it
+
+### Before vs After
+
+**Before (explicit configuration — 50+ lines):**
+
+```yaml
+open-agent-auth:
+ infrastructures:
+ trust-domain: wimse://default.trust.domain
+ key-management:
+ providers:
+ local:
+ type: in-memory
+ keys:
+ wit-verification:
+ key-id: wit-signing-key
+ algorithm: ES256
+ jwks-consumer: agent-idp
+ aoat-verification:
+ key-id: aoat-signing-key
+ algorithm: RS256
+ jwks-consumer: authorization-server
+ jwks:
+ provider:
+ enabled: true
+ consumers:
+ agent-idp:
+ enabled: true
+ issuer: http://localhost:8082
+ authorization-server:
+ enabled: true
+ issuer: http://localhost:8085
+ service-discovery:
+ services:
+ agent-idp:
+ base-url: http://localhost:8082
+ authorization-server:
+ base-url: http://localhost:8085
+ roles:
+ resource-server:
+ enabled: true
+ issuer: http://localhost:8086
+```
+
+**After (simplified with peers — 12 lines):**
+
+```yaml
+open-agent-auth:
+ roles:
+ resource-server:
+ enabled: true
+ issuer: http://localhost:8086
+ peers:
+ agent-idp:
+ issuer: http://localhost:8082
+ authorization-server:
+ issuer: http://localhost:8085
+```
+
+### Explicit Configuration Takes Precedence
+
+If you manually configure a key, JWKS consumer, or service discovery entry, the post-processor will **not** overwrite it. This ensures backward compatibility and allows fine-grained control when needed.
+
+### Role Profiles
+
+Each role has a built-in profile (`RoleProfileRegistry`) that defines its default requirements:
+
+| Role | Signing Keys | Verification Keys | Encryption Keys | Required Peers |
+|------|-------------|-------------------|-----------------|----------------|
+| `agent-idp` | wit-signing | id-token-verification | — | agent-user-idp |
+| `agent` | par-jwt-signing, vc-signing | wit-verification, id-token-verification | jwe-encryption | agent-idp, agent-user-idp, authorization-server |
+| `authorization-server` | aoat-signing | wit-verification | — (decryption: jwe-decryption) | as-user-idp, agent |
+| `resource-server` | — | wit-verification, aoat-verification | — | agent-idp, authorization-server |
+| `agent-user-idp` | id-token-signing | — | — | (none) |
+| `as-user-idp` | id-token-signing | — | — | (none) |
+
+---
+
+## OAA Configuration Discovery
+
+### Overview
+
+The `/.well-known/oaa-configuration` endpoint exposes metadata about a service instance, enabling automatic service discovery and capability negotiation between peers. This design is inspired by OIDC Discovery (`/.well-known/openid-configuration`) but tailored for multi-role agent authorization.
+
+### Endpoint
+
+```
+GET /.well-known/oaa-configuration
+```
+
+### Response Format
+
+```json
+{
+ "issuer": "http://localhost:8082",
+ "roles": ["agent-idp"],
+ "trust_domain": "wimse://default.trust.domain",
+ "protocol_version": "1.0",
+ "jwks_uri": "http://localhost:8082/.well-known/jwks.json",
+ "signing_algorithms_supported": ["ES256"],
+ "capabilities": {
+ "workload_identity": { "enabled": true }
+ },
+ "endpoints": {
+ "jwks": "http://localhost:8082/.well-known/jwks.json",
+ "authorization": "http://localhost:8082/oauth/authorize"
+ },
+ "peers_required": ["agent-user-idp"]
+}
+```
+
+### Protocol Versioning
+
+The `protocol_version` field uses semantic versioning (e.g., `"1.0"`) to ensure forward compatibility. Clients should check this field before processing the metadata.
+
+### Discovery Client
+
+The `PeerConfigurationDiscoveryClient` provides a robust mechanism for fetching peer metadata:
+
+- **Retry with exponential backoff**: Up to 3 retries with 500ms → 1s → 2s delays
+- **Fail-fast mode**: When enabled, throws `IllegalStateException` on discovery failure to prevent the application from starting with incomplete configuration
+- **Caching**: Successful discovery results are cached to avoid redundant requests
+- **Graceful degradation**: Returns `null` for 404 responses (peer doesn't expose OAA configuration), allowing fallback to explicit configuration
+
+---
+
**Maintainer**: Open Agent Auth Team
**Last Updated**: 2026-02-25
diff --git a/docs/guide/configuration/03-roles-configuration.md b/docs/guide/configuration/03-roles-configuration.md
index 0655f9d..9bb88ae 100644
--- a/docs/guide/configuration/03-roles-configuration.md
+++ b/docs/guide/configuration/03-roles-configuration.md
@@ -39,7 +39,6 @@ All roles share the following common properties:
|----------|------|-------------|----------|
| `enabled` | Boolean | Whether this role is enabled | Yes |
| `issuer` | String | Issuer URL for this role instance | Yes |
-| `instance-id` | String | Instance identifier for multi-instance deployments | No |
### Role Configuration Structure
@@ -48,8 +47,6 @@ roles:
:
enabled: boolean # Whether role is enabled
issuer: string # Issuer URL
- instance-id: string # Optional: Instance identifier
-
```
---
@@ -69,7 +66,6 @@ open-agent-auth:
agent:
enabled: true
issuer: https://agent.example.com
- instance-id: agent-001
```
#### Required Capabilities
@@ -99,7 +95,6 @@ open-agent-auth:
agent-idp:
enabled: true
issuer: https://agent-idp.example.com
- instance-id: agent-idp-001
```
#### Required Capabilities
@@ -127,7 +122,6 @@ open-agent-auth:
agent-user-idp:
enabled: true
issuer: https://agent-user-idp.example.com
- instance-id: agent-user-idp-001
```
#### Required Capabilities
@@ -156,7 +150,6 @@ open-agent-auth:
authorization-server:
enabled: true
issuer: https://auth-server.example.com
- instance-id: auth-server-001
```
#### Required Capabilities
@@ -187,7 +180,6 @@ open-agent-auth:
resource-server:
enabled: true
issuer: https://resource-server.example.com
- instance-id: resource-server-001
```
#### Required Capabilities
@@ -215,7 +207,6 @@ open-agent-auth:
as-user-idp:
enabled: true
issuer: https://as-user-idp.example.com
- instance-id: as-user-idp-001
```
#### Required Capabilities
@@ -231,35 +222,6 @@ open-agent-auth:
- **User Management**: Manage user identities
- **User Registry**: Maintain registry of authorized users
----
-
-## Multi-Instance Deployment
-
-### Overview
-
-Roles can be deployed as multiple instances using the `instance-id` property. This is useful for high availability and load balancing.
-
-### Configuration
-
-```yaml
-open-agent-auth:
- roles:
- authorization-server-primary:
- enabled: true
- issuer: https://auth-primary.example.com
- instance-id: auth-server-001
-
- authorization-server-secondary:
- enabled: true
- issuer: https://auth-secondary.example.com
- instance-id: auth-server-002
-```
-
-### Use Cases
-
-- **High Availability**: Deploy multiple instances for redundancy
-- **Load Balancing**: Distribute load across instances
-- **Geographic Distribution**: Deploy instances in different regions
---
diff --git a/docs/guide/start/02-integration-guide.md b/docs/guide/start/02-integration-guide.md
index e62317a..2471946 100644
--- a/docs/guide/start/02-integration-guide.md
+++ b/docs/guide/start/02-integration-guide.md
@@ -949,7 +949,6 @@ open-agent-auth:
roles:
resource-server:
enabled: true
- instance-id: resource-server-1
issuer: http://localhost:8086 # REQUIRED: Your Resource Server's URL
capabilities:
- resource-server
@@ -1138,7 +1137,6 @@ open-agent-auth:
roles:
as-user-idp:
enabled: true
- instance-id: as-user-idp-1
issuer: http://localhost:8084 # REQUIRED: Your AS User IDP's URL
capabilities:
- oauth2-server
diff --git a/open-agent-auth-integration-tests/scripts/run-e2e-tests.sh b/open-agent-auth-integration-tests/scripts/run-e2e-tests.sh
index b52ecf8..780755f 100755
--- a/open-agent-auth-integration-tests/scripts/run-e2e-tests.sh
+++ b/open-agent-auth-integration-tests/scripts/run-e2e-tests.sh
@@ -76,8 +76,7 @@ RESTART_ARGS=()
if [ "$DEBUG_MODE" = true ]; then
RESTART_ARGS+=("--debug")
fi
-if [ "$SKIP_BUILD" = false ]; then
- # Only pass --skip-build if we want to skip the build step in sample-start.sh
+if [ "$SKIP_BUILD" = true ]; then
RESTART_ARGS+=("--skip-build")
fi
# Add mock-llm profile to enable Mock LLM for sample-agent during E2E tests
@@ -91,13 +90,6 @@ echo ""
cd "$PROJECT_ROOT/.."
-# Set JAVA_HOME to JDK 17 if available
-JAVA_HOME_17=$(/usr/libexec/java_home -v 17 2>/dev/null)
-if [ -n "$JAVA_HOME_17" ] && [ -d "$JAVA_HOME_17" ]; then
- export JAVA_HOME="$JAVA_HOME_17"
- echo -e "${YELLOW}Using JDK 17: $JAVA_HOME${NC}"
-fi
-
if [ "$SKIP_BUILD" = false ]; then
if ! mvn clean install -DskipTests; then
echo -e "${RED}[ERROR] Failed to build project${NC}"
@@ -178,13 +170,7 @@ echo ""
cd "$PROJECT_ROOT"
-# Set JAVA_HOME to JDK 17 if available
-JAVA_HOME_17=$(/usr/libexec/java_home -v 17 2>/dev/null)
-if [ -n "$JAVA_HOME_17" ] && [ -d "$JAVA_HOME_17" ]; then
- export JAVA_HOME="$JAVA_HOME_17"
- echo -e "${YELLOW}Using JDK 17: $JAVA_HOME${NC}"
- echo ""
-fi
+
# Run the tests
TEST_START_TIME=$(date +%s)
diff --git a/open-agent-auth-samples/sample-agent-idp/src/main/resources/application.yml b/open-agent-auth-samples/sample-agent-idp/src/main/resources/application.yml
index 8a20904..91f8dfc 100644
--- a/open-agent-auth-samples/sample-agent-idp/src/main/resources/application.yml
+++ b/open-agent-auth-samples/sample-agent-idp/src/main/resources/application.yml
@@ -8,43 +8,21 @@ spring:
open-agent-auth:
enabled: true
- # Infrastructure configuration
- infrastructures:
- trust-domain: wimse://default.trust.domain
-
- key-management:
- providers:
- local:
- type: in-memory
- keys:
- wit-signing:
- key-id: wit-signing-key
- algorithm: ES256
- provider: local
- id-token-verification:
- key-id: agent-user-id-token-signing-key
- algorithm: ES256
- jwks-consumer: agent-user-idp
-
- jwks:
- provider:
- enabled: true
- consumers:
- agent-user-idp:
- enabled: true
- issuer: http://localhost:8083
+ # Role configuration
+ roles:
+ agent-idp:
+ enabled: true
+ issuer: http://localhost:8082
+
+ # Peer services (auto-configures JWKS consumers + service discovery)
+ peers:
+ agent-user-idp:
+ issuer: http://localhost:8083
# Capabilities configuration
capabilities:
workload-identity:
enabled: true
-
- # Roles configuration
- roles:
- agent-idp:
- enabled: true
- instance-id: agent-idp-1
- issuer: http://localhost:8082
logging:
level:
diff --git a/open-agent-auth-samples/sample-agent-user-idp/src/main/resources/application.yml b/open-agent-auth-samples/sample-agent-user-idp/src/main/resources/application.yml
index 4ab30a4..a4309a4 100644
--- a/open-agent-auth-samples/sample-agent-user-idp/src/main/resources/application.yml
+++ b/open-agent-auth-samples/sample-agent-user-idp/src/main/resources/application.yml
@@ -11,25 +11,13 @@ spring:
open-agent-auth:
enabled: true
-
- # Infrastructure configuration
- infrastructures:
- trust-domain: wimse://default.trust.domain
-
- key-management:
- providers:
- local:
- type: in-memory
- keys:
- id-token-signing:
- key-id: agent-user-id-token-signing-key
- algorithm: ES256
- provider: local
-
- jwks:
- provider:
- enabled: true
-
+
+ # Role configuration
+ roles:
+ agent-user-idp:
+ enabled: true
+ issuer: http://localhost:8083
+
# Capabilities configuration
capabilities:
oauth2-server:
@@ -83,13 +71,6 @@ open-agent-auth:
email: bob@agent.idp
name: Bob Johnson
- # Roles configuration
- roles:
- agent-user-idp:
- enabled: true
- instance-id: agent-user-idp-1
- issuer: http://localhost:8083
-
logging:
level:
com.alibaba.openagentauth: DEBUG
diff --git a/open-agent-auth-samples/sample-agent/src/main/resources/application.yml b/open-agent-auth-samples/sample-agent/src/main/resources/application.yml
index 0855be7..5176806 100644
--- a/open-agent-auth-samples/sample-agent/src/main/resources/application.yml
+++ b/open-agent-auth-samples/sample-agent/src/main/resources/application.yml
@@ -22,61 +22,20 @@ spring:
open-agent-auth:
enabled: true
- # Infrastructure configuration
+ # Infrastructure: only trust-domain is required; keys, JWKS consumers,
+ # service-discovery entries are all inferred from roles + peers.
infrastructures:
trust-domain: wimse://default.trust.domain
-
- key-management:
- providers:
- local:
- type: in-memory
- keys:
- # Signing keys (stored locally, contain private keys)
- par-jwt-signing:
- key-id: par-jwt-signing-key
- algorithm: RS256
- provider: local
- vc-signing:
- key-id: vc-signing-key
- algorithm: ES256
- provider: local
- jwe-encryption:
- key-id: jwe-encryption-key-001
- algorithm: RS256
- jwks-consumer: authorization-server # Public key fetched from JWKS for encryption
-
- # Verification keys (public keys fetched from JWKS)
- wit-verification:
- key-id: wit-signing-key
- algorithm: ES256
- jwks-consumer: agent-idp # Public key fetched from JWKS for verification
- id-token-verification:
- key-id: agent-user-id-token-signing-key
- algorithm: ES256
- jwks-consumer: agent-user-idp # Public key fetched from JWKS for verification
-
- jwks:
- provider:
- enabled: true
- consumers:
- agent-user-idp:
- enabled: true
- issuer: http://localhost:8083
- agent-idp:
- enabled: true
- issuer: http://localhost:8082
- authorization-server:
- enabled: true
- issuer: http://localhost:8085
- service-discovery:
- services:
- agent-user-idp:
- base-url: http://localhost:8083
- agent-idp:
- base-url: http://localhost:8082
- authorization-server:
- base-url: http://localhost:8085
+ # Peers: declare the services this agent communicates with.
+ # Each peer automatically generates JWKS consumer + service-discovery entries.
+ peers:
+ agent-user-idp:
+ issuer: http://localhost:8083
+ agent-idp:
+ issuer: http://localhost:8082
+ authorization-server:
+ issuer: http://localhost:8085
# Capabilities configuration
capabilities:
@@ -91,25 +50,18 @@ open-agent-auth:
operation-authorization:
enabled: true
- # Prompt encryption configuration (technical implementation)
prompt-encryption:
enabled: true
- encryption-key-id: jwe-encryption-key-001
- jwks-consumer: authorization-server
- # Prompt protection configuration (business logic)
prompt-protection:
enabled: true
encryption-enabled: true
sanitization-level: MEDIUM
- # Agent context configuration (runtime information)
- # These values serve as defaults and can be overridden at runtime
agent-context:
default-client: sample-agent-client
default-channel: web
default-language: zh-CN
default-platform: sample-agent.platform
default-device-fingerprint: sample-device
- # Authorization behavior configuration (authorization policy)
authorization:
require-user-interaction: false
expiration-seconds: 3600
diff --git a/open-agent-auth-samples/sample-as-user-idp/src/main/resources/application.yml b/open-agent-auth-samples/sample-as-user-idp/src/main/resources/application.yml
index 0585bcd..af7e3ff 100644
--- a/open-agent-auth-samples/sample-as-user-idp/src/main/resources/application.yml
+++ b/open-agent-auth-samples/sample-as-user-idp/src/main/resources/application.yml
@@ -12,24 +12,12 @@ spring:
open-agent-auth:
enabled: true
- # Infrastructure configuration
- infrastructures:
- trust-domain: wimse://default.trust.domain
-
- key-management:
- providers:
- local:
- type: in-memory
- keys:
- id-token-signing:
- key-id: as-user-id-token-signing-key
- algorithm: ES256
- provider: local
-
- jwks:
- provider:
- enabled: true
-
+ # Role configuration
+ roles:
+ as-user-idp:
+ enabled: true
+ issuer: http://localhost:8084
+
# Capabilities configuration
capabilities:
oauth2-server:
@@ -86,14 +74,6 @@ open-agent-auth:
subject: user_002
email: user@example.com
name: Regular User
-
- # Roles configuration
- roles:
- as-user-idp:
- enabled: true
- instance-id: as-user-idp-1
- issuer: http://localhost:8084
-
logging:
level:
com.alibaba.openagentauth: DEBUG
diff --git a/open-agent-auth-samples/sample-authorization-server/src/main/resources/application.yml b/open-agent-auth-samples/sample-authorization-server/src/main/resources/application.yml
index 1578143..64280c1 100644
--- a/open-agent-auth-samples/sample-authorization-server/src/main/resources/application.yml
+++ b/open-agent-auth-samples/sample-authorization-server/src/main/resources/application.yml
@@ -8,67 +8,26 @@ spring:
open-agent-auth:
enabled: true
- # Infrastructure configuration (shared across all roles)
+ # Infrastructure: only trust-domain is required; keys, JWKS consumers,
+ # service-discovery entries are all inferred from roles + peers.
infrastructures:
trust-domain: wimse://default.trust.domain
-
- # Key management
- key-management:
- providers:
- local:
- type: in-memory
- keys:
- # Signing keys (stored locally, contain private keys)
- aoat-signing:
- key-id: aoat-signing-key
- algorithm: RS256
- provider: local
- jwe-decryption:
- key-id: jwe-encryption-key-001
- algorithm: RS256
- provider: local
-
- # Verification keys (metadata only, actual key fetched from JWKS)
- wit-verification:
- key-id: wit-signing-key
- algorithm: ES256
- jwks-consumer: agent-idp # Public key fetched from JWKS for verification
-
- # JWKS configuration
- jwks:
- provider:
- enabled: true
- consumers:
- agent-idp:
- enabled: true
- issuer: http://localhost:8082
- agent-user-idp:
- enabled: true
- issuer: http://localhost:8083
- as-user-idp:
- enabled: true
- issuer: http://localhost:8084
- agent:
- enabled: true
- issuer: http://localhost:8081
- # Service discovery
- service-discovery:
- enabled: true
- type: static
- services:
- agent-idp:
- base-url: http://localhost:8082
- agent-user-idp:
- base-url: http://localhost:8083
- as-user-idp:
- base-url: http://localhost:8084
- agent:
- base-url: http://localhost:8081
- resource-server:
- base-url: http://localhost:8086
+ # Peers: declare the services this AS communicates with.
+ # Each peer automatically generates JWKS consumer + service-discovery entries.
+ peers:
+ agent-idp:
+ issuer: http://localhost:8082
+ agent-user-idp:
+ issuer: http://localhost:8083
+ as-user-idp:
+ issuer: http://localhost:8084
+ agent:
+ issuer: http://localhost:8081
+ resource-server:
+ issuer: http://localhost:8086
- # Capabilities configuration (composable functional features)
+ # Capabilities configuration
capabilities:
oauth2-server:
enabled: true
@@ -107,9 +66,6 @@ open-agent-auth:
enabled: true
prompt-encryption:
enabled: true
- encryption-key-id: jwe-encryption-key-001
- encryption-algorithm: RSA-OAEP-256
- content-encryption-algorithm: A256GCM
audit:
enabled: true
@@ -121,7 +77,7 @@ open-agent-auth:
enabled: true
instance-id: authorization-server-1
issuer: http://localhost:8085
-
+
logging:
level:
com.alibaba.openagentauth: DEBUG
diff --git a/open-agent-auth-samples/sample-resource-server/src/main/resources/application.yml b/open-agent-auth-samples/sample-resource-server/src/main/resources/application.yml
index b187ac4..db6522c 100644
--- a/open-agent-auth-samples/sample-resource-server/src/main/resources/application.yml
+++ b/open-agent-auth-samples/sample-resource-server/src/main/resources/application.yml
@@ -8,58 +8,19 @@ spring:
open-agent-auth:
enabled: true
- # Infrastructure configuration (shared across all roles)
- infrastructures:
- trust-domain: wimse://default.trust.domain
-
- # Key management
- key-management:
- providers:
- local:
- type: in-memory
- keys:
- # Verification keys (public keys fetched from JWKS)
- wit-verification:
- key-id: wit-signing-key
- algorithm: ES256
- jwks-consumer: agent-idp # Public key fetched from JWKS for verification
- aoat-verification:
- key-id: aoat-signing-key
- algorithm: RS256
- jwks-consumer: authorization-server # Public key fetched from JWKS for verification
-
- # JWKS configuration
- jwks:
- provider:
- enabled: true
- path: /.well-known/jwks.json
- cache-duration-seconds: 300
- cache-headers-enabled: true
- consumers:
- agent-idp:
- enabled: true
- issuer: http://localhost:8082
- authorization-server:
- enabled: true
- issuer: http://localhost:8085
-
- # Service discovery
- service-discovery:
- enabled: true
- type: static
- services:
- agent-idp:
- base-url: http://localhost:8082
- authorization-server:
- base-url: http://localhost:8085
-
- # Roles configuration
+ # Role configuration
roles:
resource-server:
enabled: true
- instance-id: resource-server-1
issuer: http://localhost:8086
+ # Peer services (auto-configures JWKS consumers + service discovery + keys)
+ peers:
+ agent-idp:
+ issuer: http://localhost:8082
+ authorization-server:
+ issuer: http://localhost:8085
+
# MCP Server Configuration
mcp:
server:
diff --git a/open-agent-auth-samples/scripts/sample-start.sh b/open-agent-auth-samples/scripts/sample-start.sh
index 530d0aa..4ffb81f 100755
--- a/open-agent-auth-samples/scripts/sample-start.sh
+++ b/open-agent-auth-samples/scripts/sample-start.sh
@@ -133,11 +133,14 @@ if [ "$SKIP_BUILD" = false ]; then
echo -e "${YELLOW}[1/7] Building project...${NC}"
# Build from parent directory and install to local repository
cd "$PROJECT_ROOT/.."
- # Force use JAVA_HOME with JDK 17 if available
- JAVA_HOME_17=$(/usr/libexec/java_home -v 17 2>/dev/null)
- if [ -n "$JAVA_HOME_17" ] && [ -d "$JAVA_HOME_17" ]; then
- export JAVA_HOME="$JAVA_HOME_17"
- echo -e "${YELLOW}Using JDK 17: $JAVA_HOME${NC}"
+ # Use JAVA_HOME with JDK 17 if available (cross-platform)
+ if command -v /usr/libexec/java_home &>/dev/null; then
+ # macOS
+ JAVA_HOME_17=$(/usr/libexec/java_home -v 17 2>/dev/null || true)
+ if [ -n "$JAVA_HOME_17" ] && [ -d "$JAVA_HOME_17" ]; then
+ export JAVA_HOME="$JAVA_HOME_17"
+ echo -e "${YELLOW}Using JDK 17: $JAVA_HOME${NC}"
+ fi
fi
# Build with spring-boot:repackage to create executable JARs
mvn clean package -DskipTests
diff --git a/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/ConfigConstants.java b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/ConfigConstants.java
index e94204c..ca86d79 100644
--- a/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/ConfigConstants.java
+++ b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/ConfigConstants.java
@@ -131,6 +131,9 @@ private ConfigConstants() {
/** Standard OIDC JWKS endpoint path. */
public static final String JWKS_WELL_KNOWN_PATH = "/.well-known/jwks.json";
+ /** OAA (Open Agent Auth) configuration metadata endpoint path. */
+ public static final String OAA_CONFIGURATION_PATH = "/.well-known/oaa-configuration";
+
// ==================== Consent Page Templates ====================
/** Thymeleaf template path for OIDC consent page. */
diff --git a/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/core/CoreAutoConfiguration.java b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/core/CoreAutoConfiguration.java
index dd370b8..7804459 100644
--- a/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/core/CoreAutoConfiguration.java
+++ b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/core/CoreAutoConfiguration.java
@@ -34,6 +34,7 @@
import com.alibaba.openagentauth.spring.autoconfigure.properties.infrastructures.JwksConsumerProperties;
import com.alibaba.openagentauth.spring.autoconfigure.properties.infrastructures.JwksInfrastructureProperties;
import com.alibaba.openagentauth.spring.autoconfigure.properties.infrastructures.KeyDefinitionProperties;
+import com.alibaba.openagentauth.spring.web.controller.JwksController;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.JWK;
import org.slf4j.Logger;
@@ -41,6 +42,7 @@
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
@@ -271,6 +273,39 @@ private Map buildJwksConsumerEndpoints(OpenAgentAuthProperties p
return endpoints;
}
+ /**
+ * Creates the JwksController bean if the JWKS provider is enabled.
+ *
+ * The JWKS provider enabled flag may be set either explicitly in YAML or
+ * automatically by the role-aware inference logic (e.g., for roles like
+ * {@code authorization-server}, {@code agent-idp}, etc.). Since the inference
+ * runs during {@code @ConfigurationProperties} binding (via
+ * {@link OpenAgentAuthProperties#afterPropertiesSet()}), the Java object property
+ * is already up-to-date when this {@code @Bean} method is evaluated.
+ *
+ *
+ * This replaces the previous {@code @ConditionalOnExpression} approach on
+ * {@link JwksController}, which checked the Spring Environment property and
+ * could not see values set by the inference logic.
+ *
+ *
+ * @param properties the configuration properties
+ * @param keyManager the key manager for retrieving active keys
+ * @return the JwksController bean, or {@code null} if JWKS provider is disabled
+ */
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
+ public JwksController jwksController(OpenAgentAuthProperties properties, KeyManager keyManager) {
+ boolean jwksProviderEnabled = properties.getInfrastructures().getJwks().getProvider().isEnabled();
+ if (!jwksProviderEnabled) {
+ logger.info("JWKS provider is disabled, skipping JwksController registration");
+ return null;
+ }
+ logger.info("Creating JwksController bean (JWKS provider enabled)");
+ return new JwksController(properties, keyManager);
+ }
+
/**
* Builds the mapping from key name to {@link KeyDefinition} from configuration properties.
*
diff --git a/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/discovery/PeerConfigurationDiscoveryClient.java b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/discovery/PeerConfigurationDiscoveryClient.java
new file mode 100644
index 0000000..a87410b
--- /dev/null
+++ b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/discovery/PeerConfigurationDiscoveryClient.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2026 Alibaba Group Holding Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.alibaba.openagentauth.spring.autoconfigure.discovery;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Client for discovering peer service configuration via the
+ * {@code /.well-known/oaa-configuration} endpoint.
+ *
+ * Note: This class is currently a reserved component for future use.
+ * It is not yet integrated into the configuration inference pipeline
+ * ({@link RoleAwareEnvironmentPostProcessor}). The current peer configuration relies
+ * on static YAML declarations (via {@code open-agent-auth.peers}). In a future release,
+ * this client will be used to dynamically discover peer capabilities at startup,
+ * enabling zero-configuration peer integration.
+ *
+ *
+ * This client implements a robust discovery mechanism with:
+ *
+ * - Retry with exponential backoff: Retries failed requests up to
+ * {@value #MAX_RETRIES} times with exponentially increasing delays
+ * - Fail-fast mode: When enabled, throws an exception on discovery
+ * failure to prevent the application from starting with incomplete configuration
+ * - Caching: Caches successful discovery results to avoid redundant requests
+ * - Timeout: Configurable connection and request timeouts
+ *
+ *
+ * @since 2.1
+ * @see RoleAwareEnvironmentPostProcessor
+ */
+public class PeerConfigurationDiscoveryClient {
+
+ private static final Logger logger = LoggerFactory.getLogger(PeerConfigurationDiscoveryClient.class);
+
+ private static final String OAA_CONFIGURATION_PATH = "/.well-known/oaa-configuration";
+ private static final int MAX_RETRIES = 3;
+ private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(5);
+ private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(10);
+ private static final long INITIAL_BACKOFF_MS = 500;
+
+ private final HttpClient httpClient;
+ private final ObjectMapper objectMapper;
+ private final boolean failFast;
+ private final Map> cache;
+
+ /**
+ * Creates a new discovery client.
+ *
+ * @param failFast if true, throws an exception when discovery fails;
+ * if false, logs a warning and returns null
+ */
+ public PeerConfigurationDiscoveryClient(boolean failFast) {
+ this.httpClient = HttpClient.newBuilder()
+ .connectTimeout(CONNECT_TIMEOUT)
+ .build();
+ this.objectMapper = new ObjectMapper();
+ this.failFast = failFast;
+ this.cache = new ConcurrentHashMap<>();
+ }
+
+ /**
+ * Discovers the OAA configuration metadata from a peer service.
+ *
+ * @param peerName the logical name of the peer (for logging)
+ * @param issuer the issuer URL of the peer service
+ * @return the metadata as a map, or null if discovery failed and failFast is false
+ * @throws IllegalStateException if discovery fails and failFast is true
+ */
+ @SuppressWarnings("unchecked")
+ public Map discover(String peerName, String issuer) {
+ Objects.requireNonNull(peerName, "peerName must not be null");
+ Objects.requireNonNull(issuer, "issuer must not be null");
+
+ Map cached = cache.get(issuer);
+ if (cached != null) {
+ logger.debug("Using cached OAA configuration for peer '{}' (issuer: {})", peerName, issuer);
+ return cached;
+ }
+
+ String discoveryUrl = issuer.endsWith("/")
+ ? issuer.substring(0, issuer.length() - 1) + OAA_CONFIGURATION_PATH
+ : issuer + OAA_CONFIGURATION_PATH;
+
+ logger.info("Discovering OAA configuration for peer '{}' from: {}", peerName, discoveryUrl);
+
+ for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
+ try {
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(discoveryUrl))
+ .timeout(REQUEST_TIMEOUT)
+ .header("Accept", "application/json")
+ .GET()
+ .build();
+
+ HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+
+ if (response.statusCode() == 200) {
+ Map metadata = objectMapper.readValue(response.body(), Map.class);
+ cache.put(issuer, metadata);
+ logger.info("Successfully discovered OAA configuration for peer '{}' "
+ + "(protocol_version: {}, roles: {})",
+ peerName,
+ metadata.get("protocol_version"),
+ metadata.get("roles"));
+ return metadata;
+ }
+
+ if (response.statusCode() == 404) {
+ logger.debug("Peer '{}' does not expose OAA configuration endpoint (404). "
+ + "Falling back to explicit configuration.", peerName);
+ return null;
+ }
+
+ logger.warn("OAA configuration discovery for peer '{}' returned HTTP {} (attempt {}/{})",
+ peerName, response.statusCode(), attempt, MAX_RETRIES);
+
+ } catch (IOException e) {
+ logger.warn("OAA configuration discovery for peer '{}' failed (attempt {}/{}): {}",
+ peerName, attempt, MAX_RETRIES, e.getMessage());
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ logger.warn("OAA configuration discovery for peer '{}' was interrupted", peerName);
+ break;
+ }
+
+ if (attempt < MAX_RETRIES) {
+ long backoffMs = INITIAL_BACKOFF_MS * (1L << (attempt - 1));
+ logger.debug("Retrying in {} ms...", backoffMs);
+ try {
+ Thread.sleep(backoffMs);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ break;
+ }
+ }
+ }
+
+ String errorMessage = String.format(
+ "Failed to discover OAA configuration for peer '%s' from %s after %d attempts. "
+ + "Ensure the peer is running and accessible, or provide explicit configuration "
+ + "under 'open-agent-auth.infrastructures'.",
+ peerName, discoveryUrl, MAX_RETRIES);
+
+ if (failFast) {
+ throw new IllegalStateException(errorMessage);
+ }
+
+ logger.warn("{} Falling back to explicit configuration.", errorMessage);
+ return null;
+ }
+
+ /**
+ * Clears the discovery cache.
+ */
+ public void clearCache() {
+ cache.clear();
+ }
+}
diff --git a/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/discovery/RoleAwareEnvironmentPostProcessor.java b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/discovery/RoleAwareEnvironmentPostProcessor.java
new file mode 100644
index 0000000..bdd2be4
--- /dev/null
+++ b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/discovery/RoleAwareEnvironmentPostProcessor.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright 2026 Alibaba Group Holding Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.alibaba.openagentauth.spring.autoconfigure.discovery;
+
+import com.alibaba.openagentauth.spring.autoconfigure.properties.InfrastructureProperties;
+import com.alibaba.openagentauth.spring.autoconfigure.properties.OpenAgentAuthProperties;
+import com.alibaba.openagentauth.spring.autoconfigure.properties.PeerProperties;
+import com.alibaba.openagentauth.spring.autoconfigure.properties.RoleProfile;
+import com.alibaba.openagentauth.spring.autoconfigure.properties.RoleProfileRegistry;
+import com.alibaba.openagentauth.spring.autoconfigure.properties.RolesProperties;
+import com.alibaba.openagentauth.spring.autoconfigure.properties.infrastructures.JwksConsumerProperties;
+import com.alibaba.openagentauth.spring.autoconfigure.properties.infrastructures.KeyDefinitionProperties;
+import com.alibaba.openagentauth.spring.autoconfigure.properties.infrastructures.KeyProviderProperties;
+import com.alibaba.openagentauth.spring.autoconfigure.properties.infrastructures.ServiceDefinitionProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Infers default infrastructure configuration from enabled roles and declared peers.
+ *
+ * This processor operates on the already-bound {@link OpenAgentAuthProperties} Java object,
+ * filling in missing configuration based on the enabled roles and declared peers. It is
+ * designed to be called after {@code @ConfigurationProperties} binding is complete
+ * but before any beans that depend on the configuration are created.
+ *
+ *
+ * This approach avoids the pitfall of injecting flat properties via {@code EnvironmentPostProcessor},
+ * which can interfere with Spring Boot's {@code @ConfigurationProperties} binding of {@code Map}
+ * types. By operating directly on the Java object, we ensure that explicit YAML configuration
+ * is preserved and only missing entries are filled in.
+ *
+ *
+ * What gets inferred:
+ *
+ * - Peer expansion: Each declared peer is expanded into a JWKS consumer
+ * and a service-discovery entry
+ * - Key inference: Based on the enabled role's profile, required signing keys,
+ * verification keys, encryption keys, and decryption keys are automatically configured
+ * - Provider defaults: If no key providers are configured, an in-memory
+ * provider is automatically added
+ * - JWKS provider: Automatically enabled if the role profile requires it
+ *
+ *
+ * Usage:
+ *
+ * This class is instantiated and invoked by {@code CoreAutoConfiguration} during its
+ * initialization, ensuring that all inferred configuration is available before any
+ * infrastructure beans (KeyManager, JwksConsumerKeyResolver, etc.) are created.
+ *
+ *
+ * @since 2.1
+ * @see RoleProfile
+ * @see RoleProfileRegistry
+ */
+public class RoleAwareEnvironmentPostProcessor {
+
+ /**
+ * Logger for the role-aware environment post-processor.
+ */
+ private static final Logger logger = LoggerFactory.getLogger(RoleAwareEnvironmentPostProcessor.class);
+
+ /**
+ * The bound configuration properties.
+ */
+ private final OpenAgentAuthProperties properties;
+
+ /**
+ * Creates a new processor for the given properties.
+ *
+ * @param properties the bound configuration properties
+ */
+ public RoleAwareEnvironmentPostProcessor(OpenAgentAuthProperties properties) {
+ this.properties = properties;
+ }
+
+ /**
+ * Processes the configuration, inferring defaults from enabled roles and declared peers.
+ *
+ * This method is idempotent — calling it multiple times has no additional effect
+ * because it only fills in entries that are not already present.
+ *
+ */
+ public void processConfiguration() {
+ List enabledRoles = findEnabledRoles();
+ if (enabledRoles.isEmpty()) {
+ logger.debug("No roles enabled, skipping role-aware configuration processing");
+ return;
+ }
+
+ logger.info("Role-aware configuration processing starting for roles: {}", enabledRoles);
+
+ InfrastructureProperties infra = properties.getInfrastructures();
+
+ // Step 1: Expand peers into JWKS consumers and service-discovery entries
+ expandPeers(infra);
+
+ // Step 2: Infer keys from role profiles
+ boolean needsJwksProvider = false;
+ for (String roleName : enabledRoles) {
+ RoleProfile profile = RoleProfileRegistry.getProfile(roleName);
+ if (profile != null) {
+ inferKeysFromProfile(infra, roleName, profile);
+ if (profile.isJwksProviderEnabled()) {
+ needsJwksProvider = true;
+ }
+ }
+ }
+
+ // Step 3: Enable JWKS provider if needed
+ if (needsJwksProvider && !infra.getJwks().getProvider().isEnabled()) {
+ infra.getJwks().getProvider().setEnabled(true);
+ logger.debug("Auto-enabled JWKS provider");
+ }
+
+ // Step 4: Ensure default key provider exists
+ ensureDefaultKeyProvider(infra);
+
+ logger.info("Role-aware configuration processing complete for roles: {}", enabledRoles);
+ }
+
+ /**
+ * Finds all roles that are explicitly enabled.
+ */
+ private List findEnabledRoles() {
+ List enabledRoles = new ArrayList<>();
+ Map roles = properties.getRoles();
+ if (roles == null || roles.isEmpty()) {
+ return enabledRoles;
+ }
+
+ for (Map.Entry entry : roles.entrySet()) {
+ if (entry.getValue() != null && entry.getValue().isEnabled()) {
+ enabledRoles.add(entry.getKey());
+ }
+ }
+ return enabledRoles;
+ }
+
+ /**
+ * Expands peer declarations into JWKS consumers and service-discovery entries.
+ * Only adds entries that are not already explicitly configured.
+ */
+ private void expandPeers(InfrastructureProperties infra) {
+ Map peers = properties.getPeers();
+ if (peers == null || peers.isEmpty()) {
+ return;
+ }
+
+ Map consumers = infra.getJwks().getConsumers();
+ Map services = infra.getServiceDiscovery().getServices();
+
+ for (Map.Entry entry : peers.entrySet()) {
+ String peerName = entry.getKey();
+ PeerProperties peer = entry.getValue();
+
+ if (peer == null || !peer.isEnabled() || peer.getIssuer() == null || peer.getIssuer().isBlank()) {
+ continue;
+ }
+
+ // Expand to JWKS consumer (only if not already configured)
+ if (!consumers.containsKey(peerName)) {
+ JwksConsumerProperties consumer = new JwksConsumerProperties();
+ consumer.setEnabled(true);
+ consumer.setIssuer(peer.getIssuer());
+ consumers.put(peerName, consumer);
+ logger.debug("Auto-configured JWKS consumer '{}' from peer (issuer: {})", peerName, peer.getIssuer());
+ }
+
+ // Expand to service-discovery entry (only if not already configured)
+ if (!services.containsKey(peerName)) {
+ ServiceDefinitionProperties service = new ServiceDefinitionProperties();
+ service.setBaseUrl(peer.getIssuer());
+ services.put(peerName, service);
+ logger.debug("Auto-configured service-discovery '{}' from peer (base-url: {})", peerName, peer.getIssuer());
+ }
+ }
+ }
+
+ /**
+ * Infers key definitions from the role profile.
+ * Only adds keys that are not already explicitly configured.
+ */
+ private void inferKeysFromProfile(InfrastructureProperties infra, String roleName, RoleProfile profile) {
+ Map keys = infra.getKeyManagement().getKeys();
+
+ // Process signing keys (local keys with private key)
+ for (String keyName : profile.getSigningKeys()) {
+ inferLocalKey(keys, keyName, profile, roleName, "signing");
+ }
+
+ // Process decryption keys (local keys with private key)
+ for (String keyName : profile.getDecryptionKeys()) {
+ inferLocalKey(keys, keyName, profile, roleName, "decryption");
+ }
+
+ // Process verification keys (remote keys from JWKS)
+ for (String keyName : profile.getVerificationKeys()) {
+ inferRemoteKey(keys, keyName, profile, roleName, "verification");
+ }
+
+ // Process encryption keys (remote public keys from JWKS)
+ for (String keyName : profile.getEncryptionKeys()) {
+ inferRemoteKey(keys, keyName, profile, roleName, "encryption");
+ }
+ }
+
+ /**
+ * Infers a local key (signing or decryption) with a local provider.
+ */
+ private void inferLocalKey(Map keys, String keyName,
+ RoleProfile profile, String roleName, String keyType) {
+ if (keys.containsKey(keyName)) {
+ return;
+ }
+
+ KeyDefinitionProperties keyDef = new KeyDefinitionProperties();
+ keyDef.setKeyId(keyName + "-key");
+ keyDef.setAlgorithm(profile.getDefaultAlgorithm(keyName));
+ keyDef.setProvider("local");
+ keys.put(keyName, keyDef);
+
+ logger.debug("Auto-configured {} key '{}' for role '{}' (algorithm: {}, provider: local)",
+ keyType, keyName, roleName, profile.getDefaultAlgorithm(keyName));
+ }
+
+ /**
+ * Infers a remote key (verification or encryption) from a JWKS consumer.
+ */
+ private void inferRemoteKey(Map keys, String keyName,
+ RoleProfile profile, String roleName, String keyType) {
+ if (keys.containsKey(keyName)) {
+ return;
+ }
+
+ String peerName = profile.getPeerForKey(keyName);
+ if (peerName == null) {
+ return;
+ }
+
+ // Derive key-id from the corresponding signing/decryption key convention
+ String correspondingKeyName = keyType.equals("verification")
+ ? keyName.replace("-verification", "-signing")
+ : keyName.replace("-encryption", "-decryption");
+
+ KeyDefinitionProperties keyDef = new KeyDefinitionProperties();
+ keyDef.setKeyId(correspondingKeyName + "-key");
+ keyDef.setAlgorithm(profile.getDefaultAlgorithm(keyName));
+ keyDef.setJwksConsumer(peerName);
+ keys.put(keyName, keyDef);
+
+ logger.debug("Auto-configured {} key '{}' for role '{}' (algorithm: {}, jwks-consumer: {})",
+ keyType, keyName, roleName, profile.getDefaultAlgorithm(keyName), peerName);
+ }
+
+ /**
+ * Ensures a default "local" key provider exists if none are configured.
+ */
+ private void ensureDefaultKeyProvider(InfrastructureProperties infra) {
+ Map providers = infra.getKeyManagement().getProviders();
+ if (!providers.containsKey("local")) {
+ KeyProviderProperties provider = new KeyProviderProperties();
+ provider.setType("in-memory");
+ providers.put("local", provider);
+ logger.debug("Auto-configured default 'local' key provider (type: in-memory)");
+ }
+ }
+}
diff --git a/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/properties/InfrastructureProperties.java b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/properties/InfrastructureProperties.java
index aadabc4..b192b2a 100644
--- a/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/properties/InfrastructureProperties.java
+++ b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/properties/InfrastructureProperties.java
@@ -18,6 +18,7 @@
import com.alibaba.openagentauth.spring.autoconfigure.properties.infrastructures.JwksInfrastructureProperties;
import com.alibaba.openagentauth.spring.autoconfigure.properties.infrastructures.KeyManagementProperties;
import com.alibaba.openagentauth.spring.autoconfigure.properties.infrastructures.ServiceDiscoveryProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
/**
* Infrastructure configuration properties for the Open Agent Auth framework.
@@ -100,6 +101,7 @@ public class InfrastructureProperties {
* tokens from other services within the trust domain.
*
*/
+ @NestedConfigurationProperty
private KeyManagementProperties keyManagement = new KeyManagementProperties();
/**
@@ -116,6 +118,7 @@ public class InfrastructureProperties {
* JWKS is used for verifying JWT signatures from other services in the trust domain.
*
*/
+ @NestedConfigurationProperty
private JwksInfrastructureProperties jwks = new JwksInfrastructureProperties();
/**
@@ -130,6 +133,7 @@ public class InfrastructureProperties {
* service instances may be dynamically scaled or relocated.
*
*/
+ @NestedConfigurationProperty
private ServiceDiscoveryProperties serviceDiscovery = new ServiceDiscoveryProperties();
/**
diff --git a/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/properties/OpenAgentAuthProperties.java b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/properties/OpenAgentAuthProperties.java
index 8d01097..d0043a4 100644
--- a/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/properties/OpenAgentAuthProperties.java
+++ b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/properties/OpenAgentAuthProperties.java
@@ -15,6 +15,10 @@
*/
package com.alibaba.openagentauth.spring.autoconfigure.properties;
+import com.alibaba.openagentauth.spring.autoconfigure.discovery.RoleAwareEnvironmentPostProcessor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
@@ -48,7 +52,6 @@
* roles:
* authorization-server:
* enabled: true
- * instance-id: auth-server-1
* issuer: http://localhost:8085
* security:
* csrf:
@@ -69,7 +72,12 @@
* @since 1.0
*/
@ConfigurationProperties(prefix = "open-agent-auth")
-public class OpenAgentAuthProperties {
+public class OpenAgentAuthProperties implements InitializingBean {
+
+ /**
+ * Logger for the Open Agent Auth properties.
+ */
+ private static final Logger logger = LoggerFactory.getLogger(OpenAgentAuthProperties.class);
/**
* Whether Open Agent Auth is enabled.
@@ -104,6 +112,31 @@ public class OpenAgentAuthProperties {
*/
private Map roles = new HashMap<>();
+ /**
+ * Peer service configurations.
+ *
+ * Peers represent other services in the trust domain that this service
+ * needs to communicate with. Declaring a peer automatically configures:
+ *
+ * - JWKS consumer for fetching the peer's public keys
+ * - Service discovery entry for the peer's base URL
+ *
+ * This eliminates the need to separately configure {@code jwks.consumers}
+ * and {@code service-discovery.services} for the same service.
+ *
+ *
+ * Configuration Example:
+ *
+ * open-agent-auth:
+ * peers:
+ * agent-idp:
+ * issuer: http://localhost:8082
+ * authorization-server:
+ * issuer: http://localhost:8085
+ *
+ */
+ private Map peers = new HashMap<>();
+
/**
* Security configuration.
*/
@@ -114,6 +147,29 @@ public class OpenAgentAuthProperties {
*/
private MonitoringProperties monitoring = new MonitoringProperties();
+ /**
+ * Called by Spring after all properties have been bound.
+ *
+ * Triggers role-aware configuration inference to fill in missing infrastructure
+ * configuration (keys, JWKS consumers, service-discovery entries) based on the
+ * enabled roles and declared peers. This ensures all inferred defaults are
+ * available before any beans that depend on the configuration are created.
+ *
+ *
+ * Since the YAML configuration has been refactored to use {@code peers} instead of
+ * directly configuring {@code infrastructures.key-management.keys} etc., the nested
+ * maps under {@code infrastructures} are expected to be empty at this point. The
+ * inference logic populates them from {@code peers} and {@code roles}.
+ *
+ */
+ @Override
+ public void afterPropertiesSet() {
+ if (enabled) {
+ logger.info("OpenAgentAuthProperties initialized, triggering role-aware configuration inference");
+ new RoleAwareEnvironmentPostProcessor(this).processConfiguration();
+ }
+ }
+
// ========== Getters and Setters ==========
/**
@@ -214,6 +270,27 @@ public void setRoles(Map roles) {
}
}
+ /**
+ * Gets the peer service configurations.
+ *
+ * @return the map of peer name to peer properties
+ */
+ public Map getPeers() {
+ return peers;
+ }
+
+ /**
+ * Sets the peer service configurations.
+ *
+ * @param peers the map of peer name to peer properties to set
+ */
+ public void setPeers(Map peers) {
+ this.peers.clear();
+ if (peers != null) {
+ this.peers.putAll(peers);
+ }
+ }
+
/**
* Gets the security configuration.
*
diff --git a/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/properties/PeerProperties.java b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/properties/PeerProperties.java
new file mode 100644
index 0000000..a4b8251
--- /dev/null
+++ b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/properties/PeerProperties.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2026 Alibaba Group Holding Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.alibaba.openagentauth.spring.autoconfigure.properties;
+
+/**
+ * Peer service configuration properties.
+ *
+ * A "peer" represents another service in the Open Agent Auth trust domain that this
+ * service needs to communicate with. By declaring a peer, the framework automatically
+ * configures:
+ *
+ * - JWKS consumer — fetches the peer's public keys for token verification
+ * - Service discovery — registers the peer's base URL for API calls
+ *
+ *
+ * This eliminates the need to separately configure {@code jwks.consumers} and
+ * {@code service-discovery.services} for the same service, reducing configuration
+ * duplication significantly.
+ *
+ *
+ * Configuration Example:
+ *
+ * open-agent-auth:
+ * peers:
+ * agent-idp:
+ * issuer: http://localhost:8082
+ * authorization-server:
+ * issuer: http://localhost:8085
+ *
+ *
+ * @since 2.1
+ */
+public class PeerProperties {
+
+ /**
+ * The issuer URL of the peer service.
+ *
+ * This URL serves as both the OIDC issuer identifier and the base URL for
+ * service discovery. The JWKS endpoint is automatically derived as
+ * {@code issuer + "/.well-known/jwks.json"}.
+ *
+ */
+ private String issuer;
+
+ /**
+ * Whether this peer is enabled.
+ *
+ * When disabled, the peer's JWKS consumer and service discovery entry
+ * will not be configured. Default: {@code true}.
+ *
+ */
+ private boolean enabled = true;
+
+ public String getIssuer() {
+ return issuer;
+ }
+
+ public void setIssuer(String issuer) {
+ this.issuer = issuer;
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+}
diff --git a/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/properties/RoleProfile.java b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/properties/RoleProfile.java
new file mode 100644
index 0000000..d342f70
--- /dev/null
+++ b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/properties/RoleProfile.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright 2026 Alibaba Group Holding Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.alibaba.openagentauth.spring.autoconfigure.properties;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Immutable role profile that defines the default configuration for a specific role.
+ *
+ * Each role in the Open Agent Auth framework has a well-defined set of requirements:
+ * signing keys, verification keys, required peers, and required capabilities.
+ * A {@code RoleProfile} captures these requirements as an immutable value object,
+ * enabling the framework to automatically infer and configure the necessary
+ * infrastructure when a role is enabled.
+ *
+ *
+ * This follows the "Convention over Configuration" principle — developers only need
+ * to declare which role they want to enable and which peers they connect to; the
+ * framework handles the rest.
+ *
+ *
+ * @since 2.1
+ */
+public final class RoleProfile {
+
+ private final List signingKeys;
+ private final List verificationKeys;
+ private final List encryptionKeys;
+ private final List decryptionKeys;
+ private final List requiredPeers;
+ private final List requiredCapabilities;
+ private final boolean jwksProviderEnabled;
+ private final Map keyDefaultAlgorithms;
+ private final Map keyToPeerMapping;
+
+ private RoleProfile(Builder builder) {
+ this.signingKeys = Collections.unmodifiableList(builder.signingKeys);
+ this.verificationKeys = Collections.unmodifiableList(builder.verificationKeys);
+ this.encryptionKeys = Collections.unmodifiableList(builder.encryptionKeys);
+ this.decryptionKeys = Collections.unmodifiableList(builder.decryptionKeys);
+ this.requiredPeers = Collections.unmodifiableList(builder.requiredPeers);
+ this.requiredCapabilities = Collections.unmodifiableList(builder.requiredCapabilities);
+ this.jwksProviderEnabled = builder.jwksProviderEnabled;
+ this.keyDefaultAlgorithms = Collections.unmodifiableMap(builder.keyDefaultAlgorithms);
+ this.keyToPeerMapping = Collections.unmodifiableMap(builder.keyToPeerMapping);
+ }
+
+ /**
+ * Returns the key names that this role needs to sign tokens with (local private keys).
+ *
+ * @return unmodifiable list of signing key names
+ */
+ public List getSigningKeys() {
+ return signingKeys;
+ }
+
+ /**
+ * Returns the key names that this role needs to verify tokens with (remote public keys).
+ *
+ * @return unmodifiable list of verification key names
+ */
+ public List getVerificationKeys() {
+ return verificationKeys;
+ }
+
+ /**
+ * Returns the key names that this role needs to encrypt data with (remote public keys).
+ *
+ * @return unmodifiable list of encryption key names
+ */
+ public List getEncryptionKeys() {
+ return encryptionKeys;
+ }
+
+ /**
+ * Returns the key names that this role needs to decrypt data with (local private keys).
+ *
+ * @return unmodifiable list of decryption key names
+ */
+ public List getDecryptionKeys() {
+ return decryptionKeys;
+ }
+
+ /**
+ * Returns the peer service names that this role depends on.
+ *
+ * @return unmodifiable list of required peer names
+ */
+ public List getRequiredPeers() {
+ return requiredPeers;
+ }
+
+ /**
+ * Returns the capability names that this role requires.
+ *
+ * @return unmodifiable list of required capability names
+ */
+ public List getRequiredCapabilities() {
+ return requiredCapabilities;
+ }
+
+ /**
+ * Returns whether this role should expose a JWKS provider endpoint.
+ *
+ * @return true if the JWKS provider should be enabled
+ */
+ public boolean isJwksProviderEnabled() {
+ return jwksProviderEnabled;
+ }
+
+ /**
+ * Gets the default algorithm for a given key name.
+ *
+ * @param keyName the key name (e.g., "wit-signing")
+ * @return the default algorithm (e.g., "ES256"), or null if not defined
+ */
+ public String getDefaultAlgorithm(String keyName) {
+ return keyDefaultAlgorithms.get(keyName);
+ }
+
+ /**
+ * Gets the peer that provides the public key for a given verification/encryption key.
+ *
+ * @param keyName the key name (e.g., "wit-verification")
+ * @return the peer name (e.g., "agent-idp"), or null if the key is local
+ */
+ public String getPeerForKey(String keyName) {
+ return keyToPeerMapping.get(keyName);
+ }
+
+ /**
+ * Gets all key names that require a specific peer.
+ *
+ * @return unmodifiable map of key name to peer name
+ */
+ public Map getKeyToPeerMapping() {
+ return keyToPeerMapping;
+ }
+
+ /**
+ * Creates a new builder for constructing a {@code RoleProfile}.
+ *
+ * @return a new builder instance
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Builder for constructing immutable {@link RoleProfile} instances.
+ *
+ * All collection fields default to empty immutable collections.
+ * The built profile wraps all collections in unmodifiable views.
+ *
+ */
+ public static final class Builder {
+ private List signingKeys = List.of();
+ private List verificationKeys = List.of();
+ private List encryptionKeys = List.of();
+ private List decryptionKeys = List.of();
+ private List requiredPeers = List.of();
+ private List requiredCapabilities = List.of();
+ private boolean jwksProviderEnabled = true;
+ private Map keyDefaultAlgorithms = Map.of();
+ private Map keyToPeerMapping = Map.of();
+
+ private Builder() {
+ }
+
+ /** Sets the signing key names for this role. */
+ public Builder signingKeys(String... keys) {
+ this.signingKeys = List.of(keys);
+ return this;
+ }
+
+ /** Sets the verification key names for this role. */
+ public Builder verificationKeys(String... keys) {
+ this.verificationKeys = List.of(keys);
+ return this;
+ }
+
+ /** Sets the encryption key names for this role. */
+ public Builder encryptionKeys(String... keys) {
+ this.encryptionKeys = List.of(keys);
+ return this;
+ }
+
+ /** Sets the decryption key names for this role. */
+ public Builder decryptionKeys(String... keys) {
+ this.decryptionKeys = List.of(keys);
+ return this;
+ }
+
+ /** Sets the required peer service names for this role. */
+ public Builder requiredPeers(String... peers) {
+ this.requiredPeers = List.of(peers);
+ return this;
+ }
+
+ /** Sets the required capability names for this role. */
+ public Builder requiredCapabilities(String... capabilities) {
+ this.requiredCapabilities = List.of(capabilities);
+ return this;
+ }
+
+ /** Sets whether the JWKS provider endpoint should be enabled. */
+ public Builder jwksProviderEnabled(boolean enabled) {
+ this.jwksProviderEnabled = enabled;
+ return this;
+ }
+
+ /**
+ * Sets the default algorithm mapping for each key name.
+ *
+ * @param algorithms map of key name to default algorithm (e.g., "ES256", "RS256")
+ * @throws NullPointerException if algorithms is null
+ */
+ public Builder keyDefaultAlgorithms(Map algorithms) {
+ this.keyDefaultAlgorithms = Map.copyOf(Objects.requireNonNull(algorithms));
+ return this;
+ }
+
+ /**
+ * Sets the mapping from key names to their source peer services.
+ *
+ * @param mapping map of key name to peer name (e.g., "wit-verification" → "agent-idp")
+ * @throws NullPointerException if mapping is null
+ */
+ public Builder keyToPeerMapping(Map mapping) {
+ this.keyToPeerMapping = Map.copyOf(Objects.requireNonNull(mapping));
+ return this;
+ }
+
+ /**
+ * Builds an immutable {@link RoleProfile} from this builder's state.
+ *
+ * @return a new RoleProfile instance
+ */
+ public RoleProfile build() {
+ return new RoleProfile(this);
+ }
+ }
+}
diff --git a/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/properties/RoleProfileRegistry.java b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/properties/RoleProfileRegistry.java
new file mode 100644
index 0000000..84d80c8
--- /dev/null
+++ b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/properties/RoleProfileRegistry.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2026 Alibaba Group Holding Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.alibaba.openagentauth.spring.autoconfigure.properties;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import static com.alibaba.openagentauth.spring.autoconfigure.ConfigConstants.*;
+
+/**
+ * Registry of built-in role profiles.
+ *
+ * This registry defines the default configuration profile for each role in the
+ * Open Agent Auth framework. When a role is enabled, the framework uses the
+ * corresponding profile to automatically infer required keys, peers, and capabilities.
+ *
+ *
+ * The profiles encode the domain knowledge about each role's requirements:
+ *
+ * - Agent IDP: Signs WITs, verifies ID Tokens from Agent User IDP
+ * - Agent: Signs PAR-JWTs and VCs, verifies WITs, encrypts prompts
+ * - Authorization Server: Signs AOATs, decrypts JWE, verifies WITs
+ * - Resource Server: Verifies WITs and AOATs
+ * - Agent User IDP: Signs ID Tokens
+ * - AS User IDP: Signs ID Tokens
+ *
+ *
+ * @since 2.1
+ */
+public final class RoleProfileRegistry {
+
+ private static final Map PROFILES;
+
+ static {
+ Map profiles = new LinkedHashMap<>();
+
+ profiles.put(ROLE_AGENT_IDP, RoleProfile.builder()
+ .signingKeys(KEY_WIT_SIGNING)
+ .verificationKeys(KEY_ID_TOKEN_VERIFICATION)
+ .requiredPeers(SERVICE_AGENT_USER_IDP)
+ .requiredCapabilities("workload-identity")
+ .jwksProviderEnabled(true)
+ .keyDefaultAlgorithms(Map.of(
+ KEY_WIT_SIGNING, "ES256",
+ KEY_ID_TOKEN_VERIFICATION, "ES256"
+ ))
+ .keyToPeerMapping(Map.of(
+ KEY_ID_TOKEN_VERIFICATION, SERVICE_AGENT_USER_IDP
+ ))
+ .build());
+
+ profiles.put(ROLE_AGENT, RoleProfile.builder()
+ .signingKeys(KEY_PAR_JWT_SIGNING, KEY_VC_SIGNING)
+ .verificationKeys(KEY_WIT_VERIFICATION, KEY_ID_TOKEN_VERIFICATION)
+ .encryptionKeys(KEY_JWE_ENCRYPTION)
+ .requiredPeers(SERVICE_AGENT_IDP, SERVICE_AGENT_USER_IDP, SERVICE_AUTHORIZATION_SERVER)
+ .requiredCapabilities("oauth2-client", "operation-authorization",
+ "operation-authorization.prompt-encryption")
+ .jwksProviderEnabled(true)
+ .keyDefaultAlgorithms(Map.of(
+ KEY_PAR_JWT_SIGNING, "RS256",
+ KEY_VC_SIGNING, "ES256",
+ KEY_WIT_VERIFICATION, "ES256",
+ KEY_ID_TOKEN_VERIFICATION, "ES256",
+ KEY_JWE_ENCRYPTION, "RS256"
+ ))
+ .keyToPeerMapping(Map.of(
+ KEY_WIT_VERIFICATION, SERVICE_AGENT_IDP,
+ KEY_ID_TOKEN_VERIFICATION, SERVICE_AGENT_USER_IDP,
+ KEY_JWE_ENCRYPTION, SERVICE_AUTHORIZATION_SERVER
+ ))
+ .build());
+
+ profiles.put(ROLE_AUTHORIZATION_SERVER, RoleProfile.builder()
+ .signingKeys(KEY_AOAT_SIGNING)
+ .verificationKeys(KEY_WIT_VERIFICATION)
+ .decryptionKeys(KEY_JWE_DECRYPTION)
+ .requiredPeers(SERVICE_AS_USER_IDP, SERVICE_AGENT)
+ .requiredCapabilities("oauth2-server", "operation-authorization",
+ "operation-authorization.prompt-encryption")
+ .jwksProviderEnabled(true)
+ .keyDefaultAlgorithms(Map.of(
+ KEY_AOAT_SIGNING, "RS256",
+ KEY_JWE_DECRYPTION, "RS256",
+ KEY_WIT_VERIFICATION, "ES256"
+ ))
+ .keyToPeerMapping(Map.of(
+ KEY_WIT_VERIFICATION, SERVICE_AGENT_IDP
+ ))
+ .build());
+
+ profiles.put(ROLE_RESOURCE_SERVER, RoleProfile.builder()
+ .verificationKeys(KEY_WIT_VERIFICATION, KEY_AOAT_VERIFICATION)
+ .requiredPeers(SERVICE_AGENT_IDP, SERVICE_AUTHORIZATION_SERVER)
+ .requiredCapabilities()
+ .jwksProviderEnabled(true)
+ .keyDefaultAlgorithms(Map.of(
+ KEY_WIT_VERIFICATION, "ES256",
+ KEY_AOAT_VERIFICATION, "RS256"
+ ))
+ .keyToPeerMapping(Map.of(
+ KEY_WIT_VERIFICATION, SERVICE_AGENT_IDP,
+ KEY_AOAT_VERIFICATION, SERVICE_AUTHORIZATION_SERVER
+ ))
+ .build());
+
+ profiles.put(ROLE_AGENT_USER_IDP, RoleProfile.builder()
+ .signingKeys(KEY_ID_TOKEN_SIGNING)
+ .jwksProviderEnabled(true)
+ .keyDefaultAlgorithms(Map.of(
+ KEY_ID_TOKEN_SIGNING, "ES256"
+ ))
+ .build());
+
+ profiles.put(ROLE_AS_USER_IDP, RoleProfile.builder()
+ .signingKeys(KEY_ID_TOKEN_SIGNING)
+ .jwksProviderEnabled(true)
+ .keyDefaultAlgorithms(Map.of(
+ KEY_ID_TOKEN_SIGNING, "ES256"
+ ))
+ .build());
+
+ PROFILES = Collections.unmodifiableMap(profiles);
+ }
+
+ private RoleProfileRegistry() {
+ throw new AssertionError("No instances");
+ }
+
+ /**
+ * Gets the role profile for the given role name.
+ *
+ * @param roleName the role name (e.g., "agent-idp")
+ * @return the role profile, or null if no profile is defined for the role
+ */
+ public static RoleProfile getProfile(String roleName) {
+ return PROFILES.get(roleName);
+ }
+
+ /**
+ * Gets all registered role profiles.
+ *
+ * @return unmodifiable map of role name to role profile
+ */
+ public static Map getAllProfiles() {
+ return PROFILES;
+ }
+}
diff --git a/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/properties/RolesProperties.java b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/properties/RolesProperties.java
index b4de732..584712f 100644
--- a/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/properties/RolesProperties.java
+++ b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/properties/RolesProperties.java
@@ -31,11 +31,9 @@
* roles:
* agent-idp:
* enabled: true
- * instance-id: agent-idp-1
* issuer: http://localhost:8082
* agent:
* enabled: true
- * instance-id: agent-1
* issuer: http://localhost:8081
*
*
@@ -109,6 +107,11 @@ public void setRoles(Map roles) {
/**
* Role configuration properties.
+ *
+ * Each role defines its identity (enabled, issuer) and the framework
+ * automatically infers the required infrastructure (keys, JWKS, service discovery)
+ * based on the role's profile.
+ *
*/
public static class RoleProperties {
@@ -117,13 +120,12 @@ public static class RoleProperties {
*/
private boolean enabled = false;
- /**
- * Instance identifier for this role (supports multiple instances).
- */
- private String instanceId;
-
/**
* Issuer URL for this role instance.
+ *
+ * The issuer URL is used as the {@code iss} claim in tokens generated by this role,
+ * and as the base URL for well-known endpoints (JWKS, OAA configuration).
+ *
*/
private String issuer;
@@ -135,14 +137,6 @@ public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
- public String getInstanceId() {
- return instanceId;
- }
-
- public void setInstanceId(String instanceId) {
- this.instanceId = instanceId;
- }
-
public String getIssuer() {
return issuer;
}
diff --git a/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/properties/infrastructures/JwksInfrastructureProperties.java b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/properties/infrastructures/JwksInfrastructureProperties.java
index 9803177..6f7a7ca 100644
--- a/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/properties/infrastructures/JwksInfrastructureProperties.java
+++ b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/properties/infrastructures/JwksInfrastructureProperties.java
@@ -78,7 +78,6 @@ public class JwksInfrastructureProperties {
* enabling efficient JWT signature validation without repeatedly fetching the keys.
*
*/
- @NestedConfigurationProperty
private Map consumers = new HashMap<>();
/**
diff --git a/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/properties/infrastructures/KeyManagementProperties.java b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/properties/infrastructures/KeyManagementProperties.java
index 22272b6..6ce2cbd 100644
--- a/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/properties/infrastructures/KeyManagementProperties.java
+++ b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/properties/infrastructures/KeyManagementProperties.java
@@ -15,8 +15,6 @@
*/
package com.alibaba.openagentauth.spring.autoconfigure.properties.infrastructures;
-import org.springframework.boot.context.properties.NestedConfigurationProperty;
-
import java.util.HashMap;
import java.util.Map;
@@ -74,7 +72,6 @@ public class KeyManagementProperties {
* Default value: empty map
*
*/
- @NestedConfigurationProperty
private Map providers = new HashMap<>();
/**
@@ -87,7 +84,6 @@ public class KeyManagementProperties {
* Default value: empty map
*
*/
- @NestedConfigurationProperty
private Map keys = new HashMap<>();
/**
diff --git a/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/properties/infrastructures/ServiceDiscoveryProperties.java b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/properties/infrastructures/ServiceDiscoveryProperties.java
index 6646578..36725f3 100644
--- a/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/properties/infrastructures/ServiceDiscoveryProperties.java
+++ b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/properties/infrastructures/ServiceDiscoveryProperties.java
@@ -16,7 +16,6 @@
package com.alibaba.openagentauth.spring.autoconfigure.properties.infrastructures;
import com.alibaba.openagentauth.spring.autoconfigure.properties.DefaultEndpoints;
-import org.springframework.boot.context.properties.NestedConfigurationProperty;
import java.util.HashMap;
import java.util.Map;
@@ -85,7 +84,6 @@ public class ServiceDiscoveryProperties {
* Default value: Empty map (no services defined)
*
*/
- @NestedConfigurationProperty
private Map services = new HashMap<>();
/**
diff --git a/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/role/AgentAutoConfiguration.java b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/role/AgentAutoConfiguration.java
index 2038218..c82c195 100644
--- a/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/role/AgentAutoConfiguration.java
+++ b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/role/AgentAutoConfiguration.java
@@ -46,8 +46,6 @@
import static com.alibaba.openagentauth.spring.autoconfigure.ConfigConstants.*;
-import com.alibaba.openagentauth.core.crypto.key.KeyManager;
-
import com.alibaba.openagentauth.framework.actor.Agent;
import com.alibaba.openagentauth.framework.executor.AgentAapExecutor;
import com.alibaba.openagentauth.framework.executor.config.AgentAapExecutorConfig;
@@ -58,6 +56,7 @@
import com.alibaba.openagentauth.framework.web.callback.OAuth2CallbackService;
import com.alibaba.openagentauth.framework.web.interceptor.AgentAuthenticationInterceptor;
import com.alibaba.openagentauth.framework.web.service.SessionMappingBizService;
+import com.alibaba.openagentauth.spring.autoconfigure.ConfigConstants;
import com.alibaba.openagentauth.spring.autoconfigure.core.CoreAutoConfiguration;
import com.alibaba.openagentauth.spring.autoconfigure.properties.OpenAgentAuthProperties;
import com.alibaba.openagentauth.spring.autoconfigure.properties.ServiceProperties;
@@ -176,12 +175,11 @@
public class AgentAutoConfiguration {
/**
- * The logger for the AgentAutoConfiguration.
- */
- private static final Logger logger = LoggerFactory.getLogger(AgentAutoConfiguration.class);
-
- /**
- * Infrastructure Configuration - manages service discovery and session-related beans.
+ * Configuration for infrastructure-related beans.
+ *
+ * This configuration provides beans for service discovery, session management,
+ * and binding instance storage.
+ *
*/
@Configuration(proxyBeanMethods = false)
static class InfrastructureConfiguration {
@@ -470,20 +468,39 @@ public OAuth2ParClient agentOperationAuthorizationParClient(
@Bean
@ConditionalOnMissingBean
public String jweEncryptionKeyId(KeyManager keyManager, OpenAgentAuthProperties openAgentAuthProperties) {
- String keyId = openAgentAuthProperties.getCapabilities().getOperationAuthorization().getPromptEncryption().getEncryptionKeyId();
- // Validate encryption key ID
+ Map keyDefinitions = openAgentAuthProperties.getInfrastructures().getKeyManagement().getKeys();
+
+ // Strategy 1: Look up by well-known key definition name "jwe-encryption" (peers-based inference)
+ KeyDefinitionProperties encryptionKeyDef = keyDefinitions.get(KEY_JWE_ENCRYPTION);
+ if (encryptionKeyDef != null && encryptionKeyDef.getKeyId() != null) {
+ String keyId = encryptionKeyDef.getKeyId();
+ logger.info("Found JWE encryption key definition '{}' with keyId: {}", KEY_JWE_ENCRYPTION, keyId);
+
+ // Remote keys (with jwks-consumer) are resolved at encryption time, no need to pre-generate
+ if (encryptionKeyDef.getJwksConsumer() == null || encryptionKeyDef.getJwksConsumer().isBlank()) {
+ try {
+ KeyAlgorithm keyAlgorithm = KeyAlgorithm.fromValue(encryptionKeyDef.getAlgorithm());
+ keyManager.getOrGenerateKey(keyId, keyAlgorithm);
+ } catch (KeyManagementException e) {
+ throw new IllegalStateException("Failed to register JWE encryption key with KeyManager", e);
+ }
+ }
+ return keyId;
+ }
+
+ // Strategy 2: Fall back to explicit encryption-key-id from capabilities config
+ String keyId = openAgentAuthProperties.getCapabilities()
+ .getOperationAuthorization().getPromptEncryption().getEncryptionKeyId();
if (keyId == null || keyId.isBlank()) {
throw new IllegalStateException("Encryption key ID must be configured via " +
- "open-agent-auth.capabilities.operation-authorization.prompt-encryption.encryption-key-id");
+ "open-agent-auth.capabilities.operation-authorization.prompt-encryption.encryption-key-id " +
+ "or inferred from peers configuration");
}
- // Get key algorithm from key definition, not from encryption-algorithm
- // encryption-algorithm is for JWE (e.g., RSA-OAEP-256), but key generation uses JWS algorithm (e.g., RS256)
- // Need to find the key definition where key-id matches the encryptionKeyId
+ // Find key algorithm from key definitions by matching keyId field
KeyAlgorithm keyAlgorithm = null;
- for (Map.Entry entry :
- openAgentAuthProperties.getInfrastructures().getKeyManagement().getKeys().entrySet()) {
+ for (Map.Entry entry : keyDefinitions.entrySet()) {
if (keyId.equals(entry.getValue().getKeyId())) {
keyAlgorithm = KeyAlgorithm.fromValue(entry.getValue().getAlgorithm());
break;
@@ -497,7 +514,6 @@ public String jweEncryptionKeyId(KeyManager keyManager, OpenAgentAuthProperties
try {
keyManager.getOrGenerateKey(keyId, keyAlgorithm);
return keyId;
-
} catch (KeyManagementException e) {
throw new IllegalStateException("Failed to register JWE encryption key with KeyManager", e);
}
diff --git a/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/role/JweEncryptionAutoConfiguration.java b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/role/JweEncryptionAutoConfiguration.java
index c263f7f..81ef73f 100644
--- a/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/role/JweEncryptionAutoConfiguration.java
+++ b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/autoconfigure/role/JweEncryptionAutoConfiguration.java
@@ -24,9 +24,13 @@
import com.alibaba.openagentauth.core.exception.crypto.KeyManagementException;
import com.alibaba.openagentauth.core.protocol.vc.jwe.PromptDecryptionService;
import com.alibaba.openagentauth.core.protocol.vc.jwe.PromptEncryptionService;
+import com.alibaba.openagentauth.spring.autoconfigure.ConfigConstants;
import com.alibaba.openagentauth.spring.autoconfigure.core.CoreAutoConfiguration;
import com.alibaba.openagentauth.spring.autoconfigure.properties.OpenAgentAuthProperties;
+import com.alibaba.openagentauth.spring.autoconfigure.properties.RoleProfile;
+import com.alibaba.openagentauth.spring.autoconfigure.properties.RoleProfileRegistry;
import com.alibaba.openagentauth.spring.autoconfigure.properties.capabilities.OperationAuthorizationProperties;
+import com.alibaba.openagentauth.spring.autoconfigure.properties.infrastructures.KeyDefinitionProperties;
import com.nimbusds.jose.EncryptionMethod;
import com.nimbusds.jose.JWEAlgorithm;
import com.nimbusds.jose.jwk.JWK;
@@ -36,8 +40,14 @@
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Condition;
+import org.springframework.context.annotation.ConditionContext;
+import org.springframework.context.annotation.Conditional;
+import org.springframework.core.env.Environment;
+import org.springframework.core.type.AnnotatedTypeMetadata;
import java.security.PrivateKey;
+import java.util.Map;
/**
* Auto-configuration for JWE encryption protection mechanism.
@@ -89,15 +99,9 @@ public JweEncoder jweEncoder(KeyManager keyManager, OpenAgentAuthProperties open
OperationAuthorizationProperties.PromptEncryptionProperties properties =
openAgentAuthProperties.getCapabilities().getOperationAuthorization().getPromptEncryption();
- String encryptionKeyId = properties.getEncryptionKeyId();
- if (encryptionKeyId == null || encryptionKeyId.isBlank()) {
- throw new IllegalStateException("Encryption key ID must be configured via " +
- "open-agent-auth.capabilities.operation-authorization.prompt-encryption.encryption-key-id");
- }
-
- logger.info("Resolving encryption key via KeyManager. Key ID: {}", encryptionKeyId);
- JWK encryptionJwk = (JWK) keyManager.resolveKey(encryptionKeyId);
- logger.info("Successfully resolved encryption key. Key ID: {}", encryptionKeyId);
+ // Resolve encryption key: first try by key definition name (supports peers-based config),
+ // then try local decryption key (AS role), then fall back to explicit encryption-key-id
+ JWK encryptionJwk = resolveEncryptionKey(keyManager, properties, openAgentAuthProperties);
JWEAlgorithm jweAlgorithm = JWEAlgorithm.parse(properties.getEncryptionAlgorithm());
EncryptionMethod encryptionMethod = EncryptionMethod.parse(properties.getContentEncryptionAlgorithm());
@@ -111,8 +115,170 @@ public JweEncoder jweEncoder(KeyManager keyManager, OpenAgentAuthProperties open
}
}
+ /**
+ * Resolves the encryption JWK using a multi-step strategy:
+ *
+ * - Try resolving by key definition name ({@code jwe-encryption}), which works with
+ * the peers-based auto-configuration where keys are inferred from role profiles.
+ * This is the typical path for the Agent role, which fetches the public key
+ * from the Authorization Server's JWKS endpoint.
+ * - If a local {@code jwe-decryption} key definition exists (typical for the
+ * Authorization Server role), use its keyId to retrieve the local key pair.
+ * The returned JWK contains both the private and public key; the public key portion
+ * is used for encryption.
+ * - Fall back to the explicit {@code encryption-key-id} from capabilities configuration,
+ * which supports legacy explicit key configuration.
+ *
+ */
+ private JWK resolveEncryptionKey(KeyManager keyManager,
+ OperationAuthorizationProperties.PromptEncryptionProperties properties,
+ OpenAgentAuthProperties openAgentAuthProperties)
+ throws KeyManagementException {
+ // Strategy 1: Resolve by key definition name "jwe-encryption" (Agent role — remote key)
+ try {
+ logger.info("Resolving encryption key by definition name: {}", ConfigConstants.KEY_JWE_ENCRYPTION);
+ JWK encryptionJwk = (JWK) keyManager.resolveKey(ConfigConstants.KEY_JWE_ENCRYPTION);
+ logger.info("Successfully resolved encryption key by definition name");
+ return encryptionJwk;
+ } catch (KeyManagementException e) {
+ logger.debug("Could not resolve encryption key by definition name, trying local decryption key", e);
+ }
+
+ // Strategy 2: Use local jwe-decryption key (Authorization Server role — local key pair)
+ // The AS role stores the decryption key locally; its public key portion is used for encryption.
+ KeyDefinitionProperties decryptionKeyDef = openAgentAuthProperties.getInfrastructures()
+ .getKeyManagement().getKeys().get(ConfigConstants.KEY_JWE_DECRYPTION);
+ if (decryptionKeyDef != null && decryptionKeyDef.getKeyId() != null) {
+ String decryptionKeyId = decryptionKeyDef.getKeyId();
+ logger.info("Resolving encryption key from local decryption key: {}", decryptionKeyId);
+ KeyAlgorithm algorithm = KeyAlgorithm.RS256;
+ keyManager.getOrGenerateKey(decryptionKeyId, algorithm);
+ JWK encryptionJwk = (JWK) keyManager.getSigningJWK(decryptionKeyId);
+ logger.info("Successfully resolved encryption key from local decryption key");
+ return encryptionJwk;
+ }
+
+ // Strategy 3: Fall back to explicit encryption-key-id
+ String encryptionKeyId = properties.getEncryptionKeyId();
+ if (encryptionKeyId == null || encryptionKeyId.isBlank()) {
+ throw new KeyManagementException(
+ "Encryption key could not be resolved. Either configure peers for automatic key inference, " +
+ "or set open-agent-auth.capabilities.operation-authorization.prompt-encryption.encryption-key-id");
+ }
+
+ logger.info("Resolving encryption key by explicit key ID: {}", encryptionKeyId);
+ JWK encryptionJwk = (JWK) keyManager.resolveKey(encryptionKeyId);
+ logger.info("Successfully resolved encryption key by explicit key ID: {}", encryptionKeyId);
+ return encryptionJwk;
+ }
+
+ /**
+ * Resolves the decryption key ID using a two-step strategy.
+ * Returns {@code null} if no decryption key is available (e.g., Agent role only encrypts).
+ */
+ static String resolveDecryptionKeyId(
+ OperationAuthorizationProperties.PromptEncryptionProperties properties,
+ OpenAgentAuthProperties openAgentAuthProperties
+ ) {
+ // Strategy 1: Look up jwe-decryption key definition from inferred config
+ KeyDefinitionProperties decryptionKeyDef = openAgentAuthProperties.getInfrastructures()
+ .getKeyManagement().getKeys().get(ConfigConstants.KEY_JWE_DECRYPTION);
+ if (decryptionKeyDef != null && decryptionKeyDef.getKeyId() != null) {
+ logger.info("Found decryption key definition '{}' with keyId: {}",
+ ConfigConstants.KEY_JWE_DECRYPTION, decryptionKeyDef.getKeyId());
+ return decryptionKeyDef.getKeyId();
+ }
+
+ // Strategy 2: Fall back to explicit encryption-key-id
+ String keyId = properties.getEncryptionKeyId();
+ if (keyId != null && !keyId.isBlank()) {
+ logger.info("Using explicit encryption key ID for decryption: {}", keyId);
+ return keyId;
+ }
+
+ // No decryption key available — this is expected for roles that only encrypt (e.g., Agent)
+ logger.info("No decryption key definition or explicit key ID found — decryption is not available");
+ return null;
+ }
+
+ /**
+ * Condition that checks whether a JWE decryption key is available.
+ *
+ * This condition evaluates to {@code true} when any of the following holds:
+ *
+ * - A {@code jwe-decryption} key definition exists explicitly in the YAML configuration.
+ * - An explicit {@code encryption-key-id} is set in the prompt-encryption configuration.
+ * - An enabled role's {@link RoleProfile} declares {@code decryptionKeys}, meaning the
+ * decryption key will be inferred at runtime by {@code RoleAwareEnvironmentPostProcessor}.
+ * This covers the Authorization Server role where the {@code jwe-decryption} key is
+ * auto-configured from peer declarations rather than explicit YAML.
+ *
+ * This ensures that {@link JweDecoder} and {@link PromptDecryptionService} beans are only
+ * registered for roles that actually perform decryption (e.g., Authorization Server), not
+ * for roles that only encrypt (e.g., Agent).
+ *
+ */
+ static class DecryptionKeyAvailableCondition implements Condition {
+
+ /**
+ * Checks whether a JWE decryption key is available.
+ *
+ * @param context the condition context
+ * @param metadata the annotated type metadata
+ * @return {@code true} if a JWE decryption key is available, {@code false} otherwise
+ */
+ @Override
+ public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
+ Environment env = context.getEnvironment();
+
+ // Strategy 1: Check if jwe-decryption key definition exists explicitly in YAML
+ String decryptionKeyId = env.getProperty(
+ "open-agent-auth.infrastructures.key-management.keys.jwe-decryption.key-id");
+ if (decryptionKeyId != null && !decryptionKeyId.isBlank()) {
+ return true;
+ }
+
+ // Strategy 2: Check explicit encryption-key-id fallback
+ String explicitKeyId = env.getProperty(
+ "open-agent-auth.capabilities.operation-authorization.prompt-encryption.encryption-key-id");
+ if (explicitKeyId != null && !explicitKeyId.isBlank()) {
+ return true;
+ }
+
+ // Strategy 3: Check if any enabled role's profile declares decryptionKeys.
+ // The decryption key will be inferred by RoleAwareEnvironmentPostProcessor at runtime,
+ // so it won't appear in the Environment yet, but we know it will be available.
+ return hasEnabledRoleWithDecryptionKeys(env);
+ }
+
+ /**
+ * Checks if any enabled role's profile declares decryptionKeys.
+ *
+ * @param env the environment
+ * @return {@code true} if any enabled role's profile declares decryptionKeys, {@code false} otherwise
+ */
+ private boolean hasEnabledRoleWithDecryptionKeys(Environment env) {
+ for (Map.Entry entry : RoleProfileRegistry.getAllProfiles().entrySet()) {
+ String roleName = entry.getKey();
+ RoleProfile profile = entry.getValue();
+ if (!profile.getDecryptionKeys().isEmpty()) {
+ String roleEnabled = env.getProperty("open-agent-auth.roles." + roleName + ".enabled");
+ if ("true".equalsIgnoreCase(roleEnabled)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ }
+
/**
* Creates the JWE decoder bean.
+ *
+ * This bean is only registered when a decryption key is available, which is determined
+ * by the {@link DecryptionKeyAvailableCondition}. The Agent role (which only encrypts)
+ * will not have this bean registered.
+ *
*
* @param keyManager the key manager for retrieving decryption keys
* @param openAgentAuthProperties the OpenAgentAuth properties
@@ -121,36 +287,27 @@ public JweEncoder jweEncoder(KeyManager keyManager, OpenAgentAuthProperties open
*/
@Bean
@ConditionalOnMissingBean
+ @Conditional(DecryptionKeyAvailableCondition.class)
public JweDecoder jweDecoder(KeyManager keyManager, OpenAgentAuthProperties openAgentAuthProperties) {
+ OperationAuthorizationProperties.PromptEncryptionProperties properties =
+ openAgentAuthProperties.getCapabilities().getOperationAuthorization().getPromptEncryption();
+
+ String keyId = resolveDecryptionKeyId(properties, openAgentAuthProperties);
+ if (keyId == null) {
+ // Should not happen due to DecryptionKeyAvailableCondition, but defensive check
+ throw new IllegalStateException("Decryption key ID resolved to null despite condition passing");
+ }
+
try {
- OperationAuthorizationProperties.PromptEncryptionProperties properties =
- openAgentAuthProperties.getCapabilities().getOperationAuthorization().getPromptEncryption();
-
- // Validate encryption key ID
- String keyId = properties.getEncryptionKeyId();
- if (keyId == null || keyId.isBlank()) {
- throw new IllegalStateException("Encryption key ID must be configured via " +
- "open-agent-auth.capabilities.operation-authorization.prompt-encryption.encryption-key-id");
- }
-
KeyAlgorithm algorithm = KeyAlgorithm.RS256;
-
- try {
- keyManager.getOrGenerateKey(keyId, algorithm);
- logger.info("JWE encryption key ready. Key ID: {}", keyId);
- } catch (KeyManagementException e) {
- logger.error("Failed to generate JWE encryption key", e);
- throw new IllegalStateException("Failed to initialize JWE encryption key", e);
- }
-
- // Retrieve decryption private key from KeyManager
- PrivateKey decryptionKey = keyManager.getSigningKey(keyId);
+ keyManager.getOrGenerateKey(keyId, algorithm);
+ logger.info("JWE decryption key ready. Key ID: {}", keyId);
+ PrivateKey decryptionKey = keyManager.getSigningKey(keyId);
logger.info("Creating JweDecoder");
-
return new NimbusJweDecoder(decryptionKey);
} catch (KeyManagementException e) {
- throw new IllegalStateException("Failed to get decryption key from KeyManager", e);
+ throw new IllegalStateException("Failed to initialize JWE decryption key: " + keyId, e);
}
}
@@ -172,6 +329,10 @@ public PromptEncryptionService promptEncryptionService(JweEncoder jweEncoder,
/**
* Creates the prompt decryption service bean.
+ *
+ * This bean is only registered when a decryption key is available (same condition as
+ * {@link #jweDecoder}). The Agent role will not have this bean registered.
+ *
*
* @param jweDecoder the JWE decoder
* @param openAgentAuthProperties the OpenAgentAuth properties
@@ -179,6 +340,7 @@ public PromptEncryptionService promptEncryptionService(JweEncoder jweEncoder,
*/
@Bean
@ConditionalOnMissingBean
+ @Conditional(DecryptionKeyAvailableCondition.class)
public PromptDecryptionService promptDecryptionService(JweDecoder jweDecoder,
OpenAgentAuthProperties openAgentAuthProperties) {
OperationAuthorizationProperties.PromptEncryptionProperties properties =
diff --git a/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/web/controller/JwksController.java b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/web/controller/JwksController.java
index 1252c3e..fa5cda0 100644
--- a/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/web/controller/JwksController.java
+++ b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/web/controller/JwksController.java
@@ -25,8 +25,6 @@
import com.nimbusds.jose.jwk.RSAKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.http.CacheControl;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
@@ -54,6 +52,13 @@
* Supports multiple signing keys (RSA, ECDSA)
* Automatic key rotation support via KeyManager
*
+ *
+ * Note: This controller is registered as a {@code @Bean} by
+ * {@link com.alibaba.openagentauth.spring.autoconfigure.core.CoreAutoConfiguration CoreAutoConfiguration}
+ * rather than via component scanning, so that the JWKS provider enabled flag set by
+ * the role-aware inference logic (which runs after {@code @ConfigurationProperties} binding)
+ * is correctly evaluated at bean-creation time.
+ *
*
* @see RFC 7517 - JSON Web Key (JWK)
* @see OpenID Connect Discovery
@@ -62,8 +67,6 @@
* @since 1.0
*/
@RestController
-@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
-@ConditionalOnExpression("'${open-agent-auth.infrastructures.jwks.provider.enabled:false}' == 'true'")
public class JwksController {
/**
diff --git a/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/web/controller/OaaConfigurationController.java b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/web/controller/OaaConfigurationController.java
new file mode 100644
index 0000000..97eb386
--- /dev/null
+++ b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/web/controller/OaaConfigurationController.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2026 Alibaba Group Holding Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.alibaba.openagentauth.spring.web.controller;
+
+import com.alibaba.openagentauth.spring.autoconfigure.ConfigConstants;
+import com.alibaba.openagentauth.spring.autoconfigure.properties.DefaultEndpoints;
+import com.alibaba.openagentauth.spring.autoconfigure.properties.OpenAgentAuthProperties;
+import com.alibaba.openagentauth.spring.autoconfigure.properties.RoleProfile;
+import com.alibaba.openagentauth.spring.autoconfigure.properties.RoleProfileRegistry;
+import com.alibaba.openagentauth.spring.autoconfigure.properties.RolesProperties;
+import com.alibaba.openagentauth.spring.web.model.OaaConfigurationMetadata;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
+import org.springframework.http.CacheControl;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * OAA Configuration Discovery Controller.
+ *
+ * Exposes the {@code /.well-known/oaa-configuration} endpoint that returns
+ * metadata about this service instance, including its role, capabilities,
+ * supported algorithms, and required peers.
+ *
+ *
+ * This endpoint enables automatic service discovery and capability negotiation
+ * between peers in the Open Agent Auth trust domain, following a design inspired
+ * by OIDC Discovery but tailored for multi-role agent authorization.
+ *
+ *
+ * @since 2.1
+ * @see OaaConfigurationMetadata
+ */
+@RestController
+@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
+@ConditionalOnProperty(prefix = "open-agent-auth", name = "enabled", havingValue = "true", matchIfMissing = true)
+public class OaaConfigurationController {
+
+ private static final Logger logger = LoggerFactory.getLogger(OaaConfigurationController.class);
+
+ /**
+ * Well-known path for OAA configuration metadata.
+ */
+ public static final String OAA_WELL_KNOWN_PATH = ConfigConstants.OAA_CONFIGURATION_PATH;
+
+ private final OpenAgentAuthProperties properties;
+
+ public OaaConfigurationController(OpenAgentAuthProperties properties) {
+ this.properties = properties;
+ }
+
+ /**
+ * Returns the OAA configuration metadata for this service instance.
+ *
+ * @return the configuration metadata with appropriate caching headers
+ */
+ @GetMapping(value = OAA_WELL_KNOWN_PATH, produces = MediaType.APPLICATION_JSON_VALUE)
+ public ResponseEntity getConfiguration() {
+ OaaConfigurationMetadata metadata = buildMetadata();
+
+ return ResponseEntity.ok()
+ .cacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES).cachePublic())
+ .body(metadata);
+ }
+
+ /**
+ * Builds the OAA configuration metadata by aggregating information from
+ * enabled roles, their profiles, and the current infrastructure configuration.
+ */
+ private OaaConfigurationMetadata buildMetadata() {
+ OaaConfigurationMetadata metadata = new OaaConfigurationMetadata();
+
+ // Determine enabled roles and their issuer
+ List enabledRoles = new ArrayList<>();
+ String issuer = null;
+ for (Map.Entry entry : properties.getRoles().entrySet()) {
+ if (entry.getValue().isEnabled()) {
+ enabledRoles.add(entry.getKey());
+ if (issuer == null && entry.getValue().getIssuer() != null) {
+ issuer = entry.getValue().getIssuer();
+ }
+ }
+ }
+
+ metadata.setIssuer(issuer);
+ metadata.setRoles(enabledRoles);
+ metadata.setTrustDomain(properties.getInfrastructures().getTrustDomain());
+ metadata.setProtocolVersion(OaaConfigurationMetadata.CURRENT_PROTOCOL_VERSION);
+
+ // JWKS URI
+ if (issuer != null) {
+ metadata.setJwksUri(issuer + ConfigConstants.JWKS_WELL_KNOWN_PATH);
+ }
+
+ // Collect signing algorithms from role profiles
+ Set algorithms = new LinkedHashSet<>();
+ Set peersRequired = new LinkedHashSet<>();
+ for (String roleName : enabledRoles) {
+ RoleProfile profile = RoleProfileRegistry.getProfile(roleName);
+ if (profile != null) {
+ for (String keyName : profile.getSigningKeys()) {
+ String algorithm = profile.getDefaultAlgorithm(keyName);
+ if (algorithm != null) {
+ algorithms.add(algorithm);
+ }
+ }
+ peersRequired.addAll(profile.getRequiredPeers());
+ }
+ }
+ metadata.setSigningAlgorithmsSupported(new ArrayList<>(algorithms));
+ metadata.setPeersRequired(new ArrayList<>(peersRequired));
+
+ // Capabilities
+ Map capabilities = buildCapabilities();
+ if (!capabilities.isEmpty()) {
+ metadata.setCapabilities(capabilities);
+ }
+
+ // Endpoints
+ Map endpoints = buildEndpoints(issuer);
+ if (!endpoints.isEmpty()) {
+ metadata.setEndpoints(endpoints);
+ }
+
+ return metadata;
+ }
+
+ /**
+ * Builds the capabilities map from enabled capability configurations.
+ * Each enabled capability is represented as a map entry with its status.
+ */
+ private Map buildCapabilities() {
+ Map capabilities = new LinkedHashMap<>();
+ var caps = properties.getCapabilities();
+
+ if (caps.getWorkloadIdentity() != null && caps.getWorkloadIdentity().isEnabled()) {
+ capabilities.put("workload_identity", Map.of("enabled", true));
+ }
+ if (caps.getOAuth2Server() != null && caps.getOAuth2Server().isEnabled()) {
+ capabilities.put("oauth2_server", Map.of("enabled", true));
+ }
+ if (caps.getOAuth2Client() != null && caps.getOAuth2Client().isEnabled()) {
+ capabilities.put("oauth2_client", Map.of("enabled", true));
+ }
+ if (caps.getOperationAuthorization() != null && caps.getOperationAuthorization().isEnabled()) {
+ capabilities.put("operation_authorization", Map.of("enabled", true));
+ }
+ if (caps.getUserAuthentication() != null && caps.getUserAuthentication().isEnabled()) {
+ capabilities.put("user_authentication", Map.of("enabled", true));
+ }
+ if (caps.getAudit() != null && caps.getAudit().isEnabled()) {
+ capabilities.put("audit", Map.of("enabled", true));
+ }
+
+ return capabilities;
+ }
+
+ /**
+ * Builds the endpoints map by combining the issuer URL with default endpoint paths.
+ *
+ * @param issuer the issuer URL to use as base, or null if no issuer is configured
+ * @return map of endpoint names to full URLs, or empty map if issuer is null
+ */
+ private Map buildEndpoints(String issuer) {
+ if (issuer == null) {
+ return Map.of();
+ }
+
+ Map endpoints = new LinkedHashMap<>();
+ Map defaults = DefaultEndpoints.getAllDefaults();
+
+ for (Map.Entry entry : defaults.entrySet()) {
+ endpoints.put(entry.getKey(), issuer + entry.getValue());
+ }
+
+ return endpoints;
+ }
+}
diff --git a/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/web/model/OaaConfigurationMetadata.java b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/web/model/OaaConfigurationMetadata.java
new file mode 100644
index 0000000..d6ef479
--- /dev/null
+++ b/open-agent-auth-spring-boot-starter/src/main/java/com/alibaba/openagentauth/spring/web/model/OaaConfigurationMetadata.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2026 Alibaba Group Holding Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.alibaba.openagentauth.spring.web.model;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * OAA (Open Agent Auth) Configuration Metadata.
+ *
+ * This model represents the metadata returned by the {@code /.well-known/oaa-configuration}
+ * endpoint. It follows a protocol-level design inspired by OIDC Discovery
+ * ({@code /.well-known/openid-configuration}) but tailored for the Open Agent Auth
+ * framework's multi-role architecture.
+ *
+ *
+ * The metadata enables automatic service discovery and capability negotiation between
+ * peers in the trust domain, significantly reducing manual configuration.
+ *
+ *
+ * Protocol Versioning
+ *
+ * The {@code protocol_version} field follows semantic versioning (e.g., "1.0").
+ * Consumers should check this field to ensure compatibility before processing
+ * the metadata.
+ *
+ *
+ * @since 2.1
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class OaaConfigurationMetadata {
+
+ /**
+ * Current protocol version for the OAA configuration metadata format.
+ */
+ public static final String CURRENT_PROTOCOL_VERSION = "1.0";
+
+ // ==================== Identity ====================
+
+ /**
+ * The issuer identifier for this service instance.
+ *
+ * This MUST be a URL using the https scheme (or http for development)
+ * that the service asserts as its Issuer Identifier.
+ *
+ */
+ @JsonProperty("issuer")
+ private String issuer;
+
+ /**
+ * The role(s) this service instance fulfills.
+ *
+ * A service may fulfill multiple roles simultaneously (e.g., "agent" + "agent-idp").
+ * Valid values: agent, agent-idp, agent-user-idp, as-user-idp,
+ * authorization-server, resource-server.
+ *
+ */
+ @JsonProperty("roles")
+ private List roles;
+
+ /**
+ * The trust domain this service belongs to.
+ *
+ * Format: {@code wimse://}
+ *
+ */
+ @JsonProperty("trust_domain")
+ private String trustDomain;
+
+ // ==================== Protocol ====================
+
+ /**
+ * The protocol version of this metadata format.
+ *
+ * Follows semantic versioning. Consumers should verify compatibility
+ * before processing the metadata.
+ *
+ */
+ @JsonProperty("protocol_version")
+ private String protocolVersion = CURRENT_PROTOCOL_VERSION;
+
+ // ==================== Key Discovery ====================
+
+ /**
+ * URL of the service's JSON Web Key Set (JWKS) document.
+ *
+ * Contains the public keys used to verify signatures issued by this service.
+ *
+ */
+ @JsonProperty("jwks_uri")
+ private String jwksUri;
+
+ /**
+ * Signing algorithms supported by this service.
+ */
+ @JsonProperty("signing_algorithms_supported")
+ private List signingAlgorithmsSupported;
+
+ // ==================== Capabilities ====================
+
+ /**
+ * The capabilities provided by this service instance.
+ *
+ * Each entry describes a functional capability and its configuration.
+ * This enables capability negotiation between peers.
+ *
+ */
+ @JsonProperty("capabilities")
+ private Map capabilities;
+
+ // ==================== Endpoints ====================
+
+ /**
+ * Service endpoints exposed by this instance.
+ *
+ * Maps endpoint names to their full URLs. Standard endpoint names include:
+ * workload.issue, oauth2.authorize, oauth2.token, oauth2.par, etc.
+ *
+ */
+ @JsonProperty("endpoints")
+ private Map endpoints;
+
+ // ==================== Peers ====================
+
+ /**
+ * Peer services required by this instance.
+ *
+ * Lists the role names of services that this instance depends on.
+ * Consumers can use this for topology-aware configuration.
+ *
+ */
+ @JsonProperty("peers_required")
+ private List peersRequired;
+
+ // ==================== Getters and Setters ====================
+
+ public String getIssuer() {
+ return issuer;
+ }
+
+ public void setIssuer(String issuer) {
+ this.issuer = issuer;
+ }
+
+ public List getRoles() {
+ return roles;
+ }
+
+ public void setRoles(List roles) {
+ this.roles = roles;
+ }
+
+ public String getTrustDomain() {
+ return trustDomain;
+ }
+
+ public void setTrustDomain(String trustDomain) {
+ this.trustDomain = trustDomain;
+ }
+
+ public String getProtocolVersion() {
+ return protocolVersion;
+ }
+
+ public void setProtocolVersion(String protocolVersion) {
+ this.protocolVersion = protocolVersion;
+ }
+
+ public String getJwksUri() {
+ return jwksUri;
+ }
+
+ public void setJwksUri(String jwksUri) {
+ this.jwksUri = jwksUri;
+ }
+
+ public List getSigningAlgorithmsSupported() {
+ return signingAlgorithmsSupported;
+ }
+
+ public void setSigningAlgorithmsSupported(List signingAlgorithmsSupported) {
+ this.signingAlgorithmsSupported = signingAlgorithmsSupported;
+ }
+
+ public Map getCapabilities() {
+ return capabilities;
+ }
+
+ public void setCapabilities(Map capabilities) {
+ this.capabilities = capabilities;
+ }
+
+ public Map getEndpoints() {
+ return endpoints;
+ }
+
+ public void setEndpoints(Map endpoints) {
+ this.endpoints = endpoints;
+ }
+
+ public List getPeersRequired() {
+ return peersRequired;
+ }
+
+ public void setPeersRequired(List peersRequired) {
+ this.peersRequired = peersRequired;
+ }
+}
diff --git a/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/autoconfigure/core/CoreAutoConfigurationTest.java b/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/autoconfigure/core/CoreAutoConfigurationTest.java
index 1c3f301..296d68d 100644
--- a/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/autoconfigure/core/CoreAutoConfigurationTest.java
+++ b/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/autoconfigure/core/CoreAutoConfigurationTest.java
@@ -25,13 +25,14 @@
import com.alibaba.openagentauth.spring.autoconfigure.properties.InfrastructureProperties;
import com.alibaba.openagentauth.spring.autoconfigure.properties.infrastructures.JwksInfrastructureProperties;
import com.alibaba.openagentauth.spring.autoconfigure.properties.infrastructures.KeyManagementProperties;
+import com.alibaba.openagentauth.spring.web.controller.JwksController;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
-import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -49,7 +50,7 @@
@DisplayName("CoreAutoConfiguration Tests")
class CoreAutoConfigurationTest {
- private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(CoreAutoConfiguration.class))
.withPropertyValues("open-agent-auth.infrastructures.trust-domain=wimse://test.trust.domain");
@@ -351,6 +352,60 @@ void shouldBindServiceDiscoveryPropertiesCorrectly() {
}
}
+ @Nested
+ @DisplayName("JwksController Bean Tests")
+ class JwksControllerBeanTests {
+
+ private final WebApplicationContextRunner webContextRunner = new WebApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(CoreAutoConfiguration.class))
+ .withPropertyValues("open-agent-auth.infrastructures.trust-domain=wimse://test.trust.domain");
+
+ @Test
+ @DisplayName("jwksController when provider enabled should create bean")
+ void jwksController_whenProviderEnabled_shouldCreateBean() {
+ webContextRunner
+ .withPropertyValues("open-agent-auth.infrastructures.jwks.provider.enabled=true")
+ .run(context -> {
+ assertThat(context).hasSingleBean(JwksController.class);
+ JwksController controller = context.getBean(JwksController.class);
+ assertThat(controller).isNotNull();
+ });
+ }
+
+ @Test
+ @DisplayName("jwksController when provider disabled should not create a usable bean")
+ void jwksController_whenProviderDisabled_shouldNotCreateBean() {
+ webContextRunner
+ .withPropertyValues("open-agent-auth.infrastructures.jwks.provider.enabled=false")
+ .run(context -> {
+ // The @Bean method returns null when JWKS provider is disabled.
+ // This is intentional because @Conditional annotations cannot see
+ // values inferred by afterPropertiesSet(), so the bean method
+ // performs a runtime check and returns null instead.
+ // getBeansOfType() excludes null beans, so the map should be empty.
+ assertThat(context.getBeansOfType(JwksController.class)).isEmpty();
+ });
+ }
+
+ @Test
+ @DisplayName("jwksController should be created when role inference enables provider")
+ void jwksController_shouldBeCreatedWhenRoleInferenceEnablesProvider() {
+ webContextRunner
+ .withPropertyValues(
+ "open-agent-auth.roles.agent-idp.enabled=true",
+ "open-agent-auth.roles.agent-idp.issuer=http://localhost:8080"
+ )
+ .run(context -> {
+ // The agent-idp role profile requires JWKS provider, so it should be auto-enabled
+ OpenAgentAuthProperties properties = context.getBean(OpenAgentAuthProperties.class);
+ assertThat(properties.getInfrastructures().getJwks().getProvider().isEnabled()).isTrue();
+
+ // JwksController should be created
+ assertThat(context).hasSingleBean(JwksController.class);
+ });
+ }
+ }
+
// Test configurations for custom beans
@Configuration
static class CustomKeyManagerConfiguration {
diff --git a/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/autoconfigure/discovery/RoleAwareEnvironmentPostProcessorTest.java b/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/autoconfigure/discovery/RoleAwareEnvironmentPostProcessorTest.java
new file mode 100644
index 0000000..a2b7b37
--- /dev/null
+++ b/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/autoconfigure/discovery/RoleAwareEnvironmentPostProcessorTest.java
@@ -0,0 +1,300 @@
+/*
+ * Copyright 2026 Alibaba Group Holding Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.alibaba.openagentauth.spring.autoconfigure.discovery;
+
+import com.alibaba.openagentauth.spring.autoconfigure.properties.InfrastructureProperties;
+import com.alibaba.openagentauth.spring.autoconfigure.properties.OpenAgentAuthProperties;
+import com.alibaba.openagentauth.spring.autoconfigure.properties.PeerProperties;
+import com.alibaba.openagentauth.spring.autoconfigure.properties.RolesProperties;
+import com.alibaba.openagentauth.spring.autoconfigure.properties.infrastructures.JwksConsumerProperties;
+import com.alibaba.openagentauth.spring.autoconfigure.properties.infrastructures.KeyDefinitionProperties;
+import com.alibaba.openagentauth.spring.autoconfigure.properties.infrastructures.KeyProviderProperties;
+import com.alibaba.openagentauth.spring.autoconfigure.properties.infrastructures.ServiceDefinitionProperties;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Unit tests for {@link RoleAwareEnvironmentPostProcessor}.
+ *
+ * @since 2.1
+ */
+class RoleAwareEnvironmentPostProcessorTest {
+
+ private OpenAgentAuthProperties properties;
+
+ @BeforeEach
+ void setUp() {
+ properties = new OpenAgentAuthProperties();
+ }
+
+ @Test
+ void noRolesConfigured_shouldNotInferAnything() {
+ RoleAwareEnvironmentPostProcessor processor = new RoleAwareEnvironmentPostProcessor(properties);
+ processor.processConfiguration();
+
+ assertEquals(0, properties.getInfrastructures().getKeyManagement().getKeys().size());
+ assertEquals(0, properties.getInfrastructures().getJwks().getConsumers().size());
+ assertEquals(0, properties.getInfrastructures().getServiceDiscovery().getServices().size());
+ }
+
+ @Test
+ void noRolesEnabled_shouldNotInferAnything() {
+ Map roles = new HashMap<>();
+ RolesProperties.RoleProperties role = new RolesProperties.RoleProperties();
+ role.setEnabled(false);
+ roles.put("agent", role);
+ properties.setRoles(roles);
+
+ RoleAwareEnvironmentPostProcessor processor = new RoleAwareEnvironmentPostProcessor(properties);
+ processor.processConfiguration();
+
+ assertEquals(0, properties.getInfrastructures().getKeyManagement().getKeys().size());
+ }
+
+ @Test
+ void agentUserIdpRole_shouldInferIdTokenSigningKey() {
+ Map roles = new HashMap<>();
+ RolesProperties.RoleProperties role = new RolesProperties.RoleProperties();
+ role.setEnabled(true);
+ role.setIssuer("http://localhost:8083");
+ roles.put("agent-user-idp", role);
+ properties.setRoles(roles);
+
+ RoleAwareEnvironmentPostProcessor processor = new RoleAwareEnvironmentPostProcessor(properties);
+ processor.processConfiguration();
+
+ Map keys = properties.getInfrastructures().getKeyManagement().getKeys();
+ assertEquals(1, keys.size());
+
+ KeyDefinitionProperties key = keys.get("id-token-signing");
+ assertNotNull(key);
+ assertEquals("id-token-signing-key", key.getKeyId());
+ assertEquals("ES256", key.getAlgorithm());
+ assertEquals("local", key.getProvider());
+ }
+
+ @Test
+ void agentIdpRoleWithPeer_shouldExpandPeers() {
+ Map roles = new HashMap<>();
+ RolesProperties.RoleProperties role = new RolesProperties.RoleProperties();
+ role.setEnabled(true);
+ role.setIssuer("http://localhost:8082");
+ roles.put("agent-idp", role);
+ properties.setRoles(roles);
+
+ Map peers = new HashMap<>();
+ PeerProperties peer = new PeerProperties();
+ peer.setEnabled(true);
+ peer.setIssuer("http://localhost:8083");
+ peers.put("agent-user-idp", peer);
+ properties.setPeers(peers);
+
+ RoleAwareEnvironmentPostProcessor processor = new RoleAwareEnvironmentPostProcessor(properties);
+ processor.processConfiguration();
+
+ Map consumers = properties.getInfrastructures().getJwks().getConsumers();
+ assertEquals(1, consumers.size());
+
+ JwksConsumerProperties consumer = consumers.get("agent-user-idp");
+ assertNotNull(consumer);
+ assertTrue(consumer.isEnabled());
+ assertEquals("http://localhost:8083", consumer.getIssuer());
+
+ Map services = properties.getInfrastructures().getServiceDiscovery().getServices();
+ assertEquals(1, services.size());
+
+ ServiceDefinitionProperties service = services.get("agent-user-idp");
+ assertNotNull(service);
+ assertEquals("http://localhost:8083", service.getBaseUrl());
+ }
+
+ @Test
+ void resourceServerRole_shouldInferVerificationKeys() {
+ Map roles = new HashMap<>();
+ RolesProperties.RoleProperties role = new RolesProperties.RoleProperties();
+ role.setEnabled(true);
+ role.setIssuer("http://localhost:8086");
+ roles.put("resource-server", role);
+ properties.setRoles(roles);
+
+ RoleAwareEnvironmentPostProcessor processor = new RoleAwareEnvironmentPostProcessor(properties);
+ processor.processConfiguration();
+
+ Map keys = properties.getInfrastructures().getKeyManagement().getKeys();
+ assertEquals(2, keys.size());
+
+ KeyDefinitionProperties witKey = keys.get("wit-verification");
+ assertNotNull(witKey);
+ assertEquals("wit-signing-key", witKey.getKeyId());
+ assertEquals("ES256", witKey.getAlgorithm());
+ assertEquals("agent-idp", witKey.getJwksConsumer());
+
+ KeyDefinitionProperties aoatKey = keys.get("aoat-verification");
+ assertNotNull(aoatKey);
+ assertEquals("aoat-signing-key", aoatKey.getKeyId());
+ assertEquals("RS256", aoatKey.getAlgorithm());
+ assertEquals("authorization-server", aoatKey.getJwksConsumer());
+ }
+
+ @Test
+ void explicitConfig_shouldNotBeOverridden() {
+ Map roles = new HashMap<>();
+ RolesProperties.RoleProperties role = new RolesProperties.RoleProperties();
+ role.setEnabled(true);
+ role.setIssuer("http://localhost:8082");
+ roles.put("agent-idp", role);
+ properties.setRoles(roles);
+
+ Map peers = new HashMap<>();
+ PeerProperties peer = new PeerProperties();
+ peer.setEnabled(true);
+ peer.setIssuer("http://localhost:8083");
+ peers.put("agent-user-idp", peer);
+ properties.setPeers(peers);
+
+ InfrastructureProperties infra = properties.getInfrastructures();
+
+ JwksConsumerProperties existingConsumer = new JwksConsumerProperties();
+ existingConsumer.setEnabled(false);
+ existingConsumer.setIssuer("http://custom.issuer.com");
+ infra.getJwks().getConsumers().put("agent-user-idp", existingConsumer);
+
+ ServiceDefinitionProperties existingService = new ServiceDefinitionProperties();
+ existingService.setBaseUrl("http://custom.baseurl.com");
+ infra.getServiceDiscovery().getServices().put("agent-user-idp", existingService);
+
+ RoleAwareEnvironmentPostProcessor processor = new RoleAwareEnvironmentPostProcessor(properties);
+ processor.processConfiguration();
+
+ JwksConsumerProperties consumer = infra.getJwks().getConsumers().get("agent-user-idp");
+ assertNotNull(consumer);
+ assertEquals("http://custom.issuer.com", consumer.getIssuer());
+
+ ServiceDefinitionProperties service = infra.getServiceDiscovery().getServices().get("agent-user-idp");
+ assertNotNull(service);
+ assertEquals("http://custom.baseurl.com", service.getBaseUrl());
+ }
+
+ @Test
+ void defaultKeyProvider_shouldBeCreated() {
+ Map roles = new HashMap<>();
+ RolesProperties.RoleProperties role = new RolesProperties.RoleProperties();
+ role.setEnabled(true);
+ role.setIssuer("http://localhost:8082");
+ roles.put("agent-idp", role);
+ properties.setRoles(roles);
+
+ RoleAwareEnvironmentPostProcessor processor = new RoleAwareEnvironmentPostProcessor(properties);
+ processor.processConfiguration();
+
+ Map providers = properties.getInfrastructures().getKeyManagement().getProviders();
+ assertEquals(1, providers.size());
+
+ KeyProviderProperties provider = providers.get("local");
+ assertNotNull(provider);
+ assertEquals("in-memory", provider.getType());
+ }
+
+ @Test
+ void jwksProvider_shouldBeAutoEnabled() {
+ Map roles = new HashMap<>();
+ RolesProperties.RoleProperties role = new RolesProperties.RoleProperties();
+ role.setEnabled(true);
+ role.setIssuer("http://localhost:8082");
+ roles.put("agent-idp", role);
+ properties.setRoles(roles);
+
+ RoleAwareEnvironmentPostProcessor processor = new RoleAwareEnvironmentPostProcessor(properties);
+ processor.processConfiguration();
+
+ assertTrue(properties.getInfrastructures().getJwks().getProvider().isEnabled());
+ }
+
+ @Test
+ void disabledPeer_shouldNotBeExpanded() {
+ Map roles = new HashMap<>();
+ RolesProperties.RoleProperties role = new RolesProperties.RoleProperties();
+ role.setEnabled(true);
+ role.setIssuer("http://localhost:8082");
+ roles.put("agent-idp", role);
+ properties.setRoles(roles);
+
+ Map peers = new HashMap<>();
+ PeerProperties peer = new PeerProperties();
+ peer.setEnabled(false);
+ peer.setIssuer("http://localhost:8083");
+ peers.put("agent-user-idp", peer);
+ properties.setPeers(peers);
+
+ RoleAwareEnvironmentPostProcessor processor = new RoleAwareEnvironmentPostProcessor(properties);
+ processor.processConfiguration();
+
+ assertEquals(0, properties.getInfrastructures().getJwks().getConsumers().size());
+ assertEquals(0, properties.getInfrastructures().getServiceDiscovery().getServices().size());
+ }
+
+ @Test
+ void peerWithNullIssuer_shouldNotBeExpanded() {
+ Map roles = new HashMap<>();
+ RolesProperties.RoleProperties role = new RolesProperties.RoleProperties();
+ role.setEnabled(true);
+ role.setIssuer("http://localhost:8082");
+ roles.put("agent-idp", role);
+ properties.setRoles(roles);
+
+ Map peers = new HashMap<>();
+ PeerProperties peer = new PeerProperties();
+ peer.setEnabled(true);
+ peer.setIssuer(null);
+ peers.put("agent-user-idp", peer);
+ properties.setPeers(peers);
+
+ RoleAwareEnvironmentPostProcessor processor = new RoleAwareEnvironmentPostProcessor(properties);
+ processor.processConfiguration();
+
+ assertEquals(0, properties.getInfrastructures().getJwks().getConsumers().size());
+ assertEquals(0, properties.getInfrastructures().getServiceDiscovery().getServices().size());
+ }
+
+ @Test
+ void agentRole_shouldInferMultipleKeys() {
+ Map roles = new HashMap<>();
+ RolesProperties.RoleProperties role = new RolesProperties.RoleProperties();
+ role.setEnabled(true);
+ role.setIssuer("http://localhost:8081");
+ roles.put("agent", role);
+ properties.setRoles(roles);
+
+ RoleAwareEnvironmentPostProcessor processor = new RoleAwareEnvironmentPostProcessor(properties);
+ processor.processConfiguration();
+
+ Map keys = properties.getInfrastructures().getKeyManagement().getKeys();
+ assertEquals(5, keys.size());
+
+ assertNotNull(keys.get("par-jwt-signing"));
+ assertNotNull(keys.get("vc-signing"));
+ assertNotNull(keys.get("wit-verification"));
+ assertNotNull(keys.get("id-token-verification"));
+ assertNotNull(keys.get("jwe-encryption"));
+ }
+}
diff --git a/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/autoconfigure/properties/OpenAgentAuthPropertiesTest.java b/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/autoconfigure/properties/OpenAgentAuthPropertiesTest.java
index b2c5db8..8df09f2 100644
--- a/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/autoconfigure/properties/OpenAgentAuthPropertiesTest.java
+++ b/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/autoconfigure/properties/OpenAgentAuthPropertiesTest.java
@@ -20,6 +20,9 @@
import com.alibaba.openagentauth.spring.autoconfigure.properties.capabilities.OperationAuthorizationProperties;
import com.alibaba.openagentauth.spring.autoconfigure.properties.capabilities.UserAuthenticationProperties;
import com.alibaba.openagentauth.spring.autoconfigure.properties.capabilities.WorkloadIdentityProperties;
+import com.alibaba.openagentauth.spring.autoconfigure.properties.infrastructures.JwksConsumerProperties;
+import com.alibaba.openagentauth.spring.autoconfigure.properties.infrastructures.KeyDefinitionProperties;
+import com.alibaba.openagentauth.spring.autoconfigure.properties.infrastructures.ServiceDefinitionProperties;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
@@ -28,6 +31,7 @@
import java.util.HashMap;
import java.util.Map;
+import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
/**
@@ -264,22 +268,12 @@ void shouldSetAndGetRolesMap() {
void shouldCreateAndConfigureRoleProperties() {
RolesProperties.RoleProperties role = new RolesProperties.RoleProperties();
role.setEnabled(true);
- role.setInstanceId("instance-1");
role.setIssuer("https://example.com/issuer");
assertTrue(role.isEnabled());
- assertEquals("instance-1", role.getInstanceId());
assertEquals("https://example.com/issuer", role.getIssuer());
}
- @Test
- @DisplayName("Should handle null instance id")
- void shouldHandleNullInstanceId() {
- RolesProperties.RoleProperties role = new RolesProperties.RoleProperties();
- role.setInstanceId(null);
- assertNull(role.getInstanceId());
- }
-
@Test
@DisplayName("Should handle null issuer")
void shouldHandleNullIssuer() {
@@ -324,4 +318,157 @@ void shouldCreateIndependentInstances() {
assertTrue(properties2.isEnabled());
}
}
+
+ @Nested
+ @DisplayName("InitializingBean and Role-Aware Inference Tests")
+ class InitializingBeanAndRoleAwareInferenceTests {
+
+ @Test
+ @DisplayName("afterPropertiesSet when enabled should trigger inference")
+ void afterPropertiesSet_whenEnabled_shouldTriggerInference() {
+ properties.setEnabled(true);
+ properties.setInfrastructures(new InfrastructureProperties());
+
+ // Add an enabled role
+ Map roles = new HashMap<>();
+ RolesProperties.RoleProperties role = new RolesProperties.RoleProperties();
+ role.setEnabled(true);
+ role.setIssuer("http://localhost:8080");
+ roles.put("agent-idp", role);
+ properties.setRoles(roles);
+
+ // Call afterPropertiesSet
+ properties.afterPropertiesSet();
+
+ // Verify that inference has occurred - JWKS provider should be enabled for agent-idp role
+ assertThat(properties.getInfrastructures().getJwks().getProvider().isEnabled()).isTrue();
+ }
+
+ @Test
+ @DisplayName("afterPropertiesSet when disabled should not trigger inference")
+ void afterPropertiesSet_whenDisabled_shouldNotTriggerInference() {
+ properties.setEnabled(false);
+ properties.setInfrastructures(new InfrastructureProperties());
+
+ // Add an enabled role that would normally trigger key inference
+ Map roles = new HashMap<>();
+ RolesProperties.RoleProperties role = new RolesProperties.RoleProperties();
+ role.setEnabled(true);
+ role.setIssuer("http://localhost:8080");
+ roles.put("agent-idp", role);
+ properties.setRoles(roles);
+
+ // Call afterPropertiesSet
+ properties.afterPropertiesSet();
+
+ // Verify that inference has NOT occurred - no keys should be inferred
+ assertThat(properties.getInfrastructures().getKeyManagement().getKeys()).isEmpty();
+ }
+
+ @Test
+ @DisplayName("afterPropertiesSet should infer keys from roles")
+ void afterPropertiesSet_shouldInferKeysFromRoles() {
+ properties.setEnabled(true);
+ properties.setInfrastructures(new InfrastructureProperties());
+
+ // Add an enabled role that requires keys
+ Map roles = new HashMap<>();
+ RolesProperties.RoleProperties role = new RolesProperties.RoleProperties();
+ role.setEnabled(true);
+ role.setIssuer("http://localhost:8080");
+ roles.put("agent-idp", role);
+ properties.setRoles(roles);
+
+ // Call afterPropertiesSet
+ properties.afterPropertiesSet();
+
+ // Verify that keys have been inferred
+ Map keys = properties.getInfrastructures().getKeyManagement().getKeys();
+ assertThat(keys).isNotEmpty();
+
+ // Verify that a local provider was added
+ assertThat(properties.getInfrastructures().getKeyManagement().getProviders()).containsKey("local");
+ }
+
+ @Test
+ @DisplayName("afterPropertiesSet should expand peers to JWKS consumers")
+ void afterPropertiesSet_shouldExpandPeersToJwksConsumers() {
+ properties.setEnabled(true);
+ properties.setInfrastructures(new InfrastructureProperties());
+
+ // Must have at least one enabled role for processConfiguration to run
+ Map roles = new HashMap<>();
+ RolesProperties.RoleProperties role = new RolesProperties.RoleProperties();
+ role.setEnabled(true);
+ role.setIssuer("http://localhost:8080");
+ roles.put("agent-idp", role);
+ properties.setRoles(roles);
+
+ // Add a peer configuration
+ Map peers = new HashMap<>();
+ PeerProperties peer = new PeerProperties();
+ peer.setEnabled(true);
+ peer.setIssuer("http://localhost:8082");
+ peers.put("authorization-server", peer);
+ properties.setPeers(peers);
+
+ // Call afterPropertiesSet
+ properties.afterPropertiesSet();
+
+ // Verify that peer was expanded to JWKS consumer
+ Map consumers = properties.getInfrastructures().getJwks().getConsumers();
+ assertThat(consumers).containsKey("authorization-server");
+ assertThat(consumers.get("authorization-server").getIssuer()).isEqualTo("http://localhost:8082");
+
+ // Verify that peer was expanded to service-discovery entry
+ Map services = properties.getInfrastructures().getServiceDiscovery().getServices();
+ assertThat(services).containsKey("authorization-server");
+ assertThat(services.get("authorization-server").getBaseUrl()).isEqualTo("http://localhost:8082");
+ }
+
+ @Test
+ @DisplayName("afterPropertiesSet should not override explicit config")
+ void afterPropertiesSet_shouldNotOverrideExplicitConfig() {
+ properties.setEnabled(true);
+ properties.setInfrastructures(new InfrastructureProperties());
+
+ // Must have at least one enabled role for processConfiguration to run
+ Map roles = new HashMap<>();
+ RolesProperties.RoleProperties role = new RolesProperties.RoleProperties();
+ role.setEnabled(true);
+ role.setIssuer("http://localhost:8080");
+ roles.put("agent-idp", role);
+ properties.setRoles(roles);
+
+ // Add explicit JWKS consumer configuration
+ JwksConsumerProperties explicitConsumer = new JwksConsumerProperties();
+ explicitConsumer.setEnabled(true);
+ explicitConsumer.setIssuer("http://explicit-config.example.com");
+ properties.getInfrastructures().getJwks().getConsumers().put("authorization-server", explicitConsumer);
+
+ // Add explicit service-discovery configuration
+ ServiceDefinitionProperties explicitService = new ServiceDefinitionProperties();
+ explicitService.setBaseUrl("http://explicit-service.example.com");
+ properties.getInfrastructures().getServiceDiscovery().getServices().put("authorization-server", explicitService);
+
+ // Add a peer with different issuer
+ Map peers = new HashMap<>();
+ PeerProperties peer = new PeerProperties();
+ peer.setEnabled(true);
+ peer.setIssuer("http://localhost:8082");
+ peers.put("authorization-server", peer);
+ properties.setPeers(peers);
+
+ // Call afterPropertiesSet
+ properties.afterPropertiesSet();
+
+ // Verify that explicit JWKS consumer was NOT overridden
+ Map consumers = properties.getInfrastructures().getJwks().getConsumers();
+ assertThat(consumers.get("authorization-server").getIssuer()).isEqualTo("http://explicit-config.example.com");
+
+ // Verify that explicit service-discovery was NOT overridden
+ Map services = properties.getInfrastructures().getServiceDiscovery().getServices();
+ assertThat(services.get("authorization-server").getBaseUrl()).isEqualTo("http://explicit-service.example.com");
+ }
+ }
}
\ No newline at end of file
diff --git a/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/autoconfigure/properties/PeerPropertiesTest.java b/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/autoconfigure/properties/PeerPropertiesTest.java
new file mode 100644
index 0000000..276a6c9
--- /dev/null
+++ b/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/autoconfigure/properties/PeerPropertiesTest.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2026 Alibaba Group Holding Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.alibaba.openagentauth.spring.autoconfigure.properties;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Unit tests for {@link PeerProperties}.
+ *
+ * @since 2.1
+ */
+@SpringBootTest(classes = TestConfiguration.class)
+class PeerPropertiesTest {
+
+ @Test
+ void testDefaultValues() {
+ PeerProperties properties = new PeerProperties();
+
+ assertTrue(properties.isEnabled());
+ }
+
+ @Test
+ void testGetterSetter() {
+ PeerProperties properties = new PeerProperties();
+
+ properties.setIssuer("http://localhost:8082");
+ assertEquals("http://localhost:8082", properties.getIssuer());
+
+ properties.setEnabled(false);
+ assertEquals(false, properties.isEnabled());
+
+ properties.setEnabled(true);
+ assertEquals(true, properties.isEnabled());
+ }
+
+ @Test
+ void testEnabledDefaultValueIsTrue() {
+ PeerProperties properties = new PeerProperties();
+
+ assertTrue(properties.isEnabled());
+ }
+
+ @Test
+ void testIssuerSetting() {
+ PeerProperties properties = new PeerProperties();
+
+ String issuer1 = "http://localhost:8082";
+ properties.setIssuer(issuer1);
+ assertEquals(issuer1, properties.getIssuer());
+
+ String issuer2 = "https://agent-idp.example.com";
+ properties.setIssuer(issuer2);
+ assertEquals(issuer2, properties.getIssuer());
+
+ String issuer3 = "http://192.168.1.100:8080";
+ properties.setIssuer(issuer3);
+ assertEquals(issuer3, properties.getIssuer());
+ }
+}
diff --git a/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/autoconfigure/properties/RoleProfileRegistryTest.java b/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/autoconfigure/properties/RoleProfileRegistryTest.java
new file mode 100644
index 0000000..c91f2e8
--- /dev/null
+++ b/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/autoconfigure/properties/RoleProfileRegistryTest.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright 2026 Alibaba Group Holding Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.alibaba.openagentauth.spring.autoconfigure.properties;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import java.util.List;
+
+import static com.alibaba.openagentauth.spring.autoconfigure.ConfigConstants.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Unit tests for {@link RoleProfileRegistry}.
+ *
+ * @since 2.1
+ */
+@SpringBootTest(classes = TestConfiguration.class)
+class RoleProfileRegistryTest {
+
+ @Test
+ void testAllSixRolesExist() {
+ assertNotNull(RoleProfileRegistry.getProfile(ROLE_AGENT_IDP));
+ assertNotNull(RoleProfileRegistry.getProfile(ROLE_AGENT));
+ assertNotNull(RoleProfileRegistry.getProfile(ROLE_AUTHORIZATION_SERVER));
+ assertNotNull(RoleProfileRegistry.getProfile(ROLE_RESOURCE_SERVER));
+ assertNotNull(RoleProfileRegistry.getProfile(ROLE_AGENT_USER_IDP));
+ assertNotNull(RoleProfileRegistry.getProfile(ROLE_AS_USER_IDP));
+ }
+
+ @Test
+ void testGetProfileReturnsNullForUnknownRole() {
+ assertNull(RoleProfileRegistry.getProfile("unknown-role"));
+ assertNull(RoleProfileRegistry.getProfile("non-existent"));
+ assertNull(RoleProfileRegistry.getProfile(""));
+ }
+
+ @Test
+ void testGetAllProfilesReturnsImmutableMap() {
+ assertThrows(UnsupportedOperationException.class, () ->
+ RoleProfileRegistry.getAllProfiles().put("new-role", RoleProfile.builder().build())
+ );
+ }
+
+ @Test
+ void testGetAllProfilesSize() {
+ assertEquals(6, RoleProfileRegistry.getAllProfiles().size());
+ }
+
+ @Test
+ void testAgentIdpProfile() {
+ RoleProfile profile = RoleProfileRegistry.getProfile(ROLE_AGENT_IDP);
+
+ assertEquals(List.of(KEY_WIT_SIGNING), profile.getSigningKeys());
+ assertEquals(List.of(KEY_ID_TOKEN_VERIFICATION), profile.getVerificationKeys());
+ assertEquals(List.of(SERVICE_AGENT_USER_IDP), profile.getRequiredPeers());
+ assertEquals(List.of("workload-identity"), profile.getRequiredCapabilities());
+ assertEquals(true, profile.isJwksProviderEnabled());
+ assertEquals("ES256", profile.getDefaultAlgorithm(KEY_WIT_SIGNING));
+ assertEquals("ES256", profile.getDefaultAlgorithm(KEY_ID_TOKEN_VERIFICATION));
+ assertEquals(SERVICE_AGENT_USER_IDP, profile.getPeerForKey(KEY_ID_TOKEN_VERIFICATION));
+ }
+
+ @Test
+ void testAgentProfile() {
+ RoleProfile profile = RoleProfileRegistry.getProfile(ROLE_AGENT);
+
+ assertEquals(List.of(KEY_PAR_JWT_SIGNING, KEY_VC_SIGNING), profile.getSigningKeys());
+ assertEquals(List.of(KEY_WIT_VERIFICATION, KEY_ID_TOKEN_VERIFICATION), profile.getVerificationKeys());
+ assertEquals(List.of(KEY_JWE_ENCRYPTION), profile.getEncryptionKeys());
+ assertEquals(List.of(SERVICE_AGENT_IDP, SERVICE_AGENT_USER_IDP, SERVICE_AUTHORIZATION_SERVER), profile.getRequiredPeers());
+ assertEquals(List.of("oauth2-client", "operation-authorization", "operation-authorization.prompt-encryption"), profile.getRequiredCapabilities());
+ assertEquals(true, profile.isJwksProviderEnabled());
+
+ assertEquals("RS256", profile.getDefaultAlgorithm(KEY_PAR_JWT_SIGNING));
+ assertEquals("ES256", profile.getDefaultAlgorithm(KEY_VC_SIGNING));
+ assertEquals("ES256", profile.getDefaultAlgorithm(KEY_WIT_VERIFICATION));
+ assertEquals("ES256", profile.getDefaultAlgorithm(KEY_ID_TOKEN_VERIFICATION));
+ assertEquals("RS256", profile.getDefaultAlgorithm(KEY_JWE_ENCRYPTION));
+
+ assertEquals(SERVICE_AGENT_IDP, profile.getPeerForKey(KEY_WIT_VERIFICATION));
+ assertEquals(SERVICE_AGENT_USER_IDP, profile.getPeerForKey(KEY_ID_TOKEN_VERIFICATION));
+ assertEquals(SERVICE_AUTHORIZATION_SERVER, profile.getPeerForKey(KEY_JWE_ENCRYPTION));
+ }
+
+ @Test
+ void testAuthorizationServerProfile() {
+ RoleProfile profile = RoleProfileRegistry.getProfile(ROLE_AUTHORIZATION_SERVER);
+
+ assertEquals(List.of(KEY_AOAT_SIGNING), profile.getSigningKeys());
+ assertEquals(List.of(KEY_WIT_VERIFICATION), profile.getVerificationKeys());
+ assertEquals(List.of(KEY_JWE_DECRYPTION), profile.getDecryptionKeys());
+ assertEquals(List.of(SERVICE_AS_USER_IDP, SERVICE_AGENT), profile.getRequiredPeers());
+ assertEquals(List.of("oauth2-server", "operation-authorization", "operation-authorization.prompt-encryption"), profile.getRequiredCapabilities());
+ assertEquals(true, profile.isJwksProviderEnabled());
+
+ assertEquals("RS256", profile.getDefaultAlgorithm(KEY_AOAT_SIGNING));
+ assertEquals("RS256", profile.getDefaultAlgorithm(KEY_JWE_DECRYPTION));
+ assertEquals("ES256", profile.getDefaultAlgorithm(KEY_WIT_VERIFICATION));
+
+ assertEquals(SERVICE_AGENT_IDP, profile.getPeerForKey(KEY_WIT_VERIFICATION));
+ }
+
+ @Test
+ void testResourceServerProfile() {
+ RoleProfile profile = RoleProfileRegistry.getProfile(ROLE_RESOURCE_SERVER);
+
+ assertEquals(List.of(KEY_WIT_VERIFICATION, KEY_AOAT_VERIFICATION), profile.getVerificationKeys());
+ assertEquals(List.of(SERVICE_AGENT_IDP, SERVICE_AUTHORIZATION_SERVER), profile.getRequiredPeers());
+ assertEquals(true, profile.isJwksProviderEnabled());
+
+ assertEquals("ES256", profile.getDefaultAlgorithm(KEY_WIT_VERIFICATION));
+ assertEquals("RS256", profile.getDefaultAlgorithm(KEY_AOAT_VERIFICATION));
+
+ assertEquals(SERVICE_AGENT_IDP, profile.getPeerForKey(KEY_WIT_VERIFICATION));
+ assertEquals(SERVICE_AUTHORIZATION_SERVER, profile.getPeerForKey(KEY_AOAT_VERIFICATION));
+ }
+
+ @Test
+ void testAgentUserIdpProfile() {
+ RoleProfile profile = RoleProfileRegistry.getProfile(ROLE_AGENT_USER_IDP);
+
+ assertEquals(List.of(KEY_ID_TOKEN_SIGNING), profile.getSigningKeys());
+ assertEquals(true, profile.isJwksProviderEnabled());
+
+ assertEquals("ES256", profile.getDefaultAlgorithm(KEY_ID_TOKEN_SIGNING));
+ }
+
+ @Test
+ void testAsUserIdpProfile() {
+ RoleProfile profile = RoleProfileRegistry.getProfile(ROLE_AS_USER_IDP);
+
+ assertEquals(List.of(KEY_ID_TOKEN_SIGNING), profile.getSigningKeys());
+ assertEquals(true, profile.isJwksProviderEnabled());
+
+ assertEquals("ES256", profile.getDefaultAlgorithm(KEY_ID_TOKEN_SIGNING));
+ }
+
+ @Test
+ void testRegistryCannotBeInstantiated() throws Exception {
+ var constructor = RoleProfileRegistry.class.getDeclaredConstructor();
+ constructor.setAccessible(true);
+ assertThrows(java.lang.reflect.InvocationTargetException.class, constructor::newInstance);
+ }
+
+ @Test
+ void testProfileImmutability() {
+ RoleProfile profile = RoleProfileRegistry.getProfile(ROLE_AGENT);
+
+ assertThrows(UnsupportedOperationException.class, () ->
+ profile.getSigningKeys().add("new-key")
+ );
+
+ assertThrows(UnsupportedOperationException.class, () ->
+ profile.getVerificationKeys().add("new-key")
+ );
+
+ assertThrows(UnsupportedOperationException.class, () ->
+ profile.getEncryptionKeys().add("new-key")
+ );
+
+ assertThrows(UnsupportedOperationException.class, () ->
+ profile.getDecryptionKeys().add("new-key")
+ );
+
+ assertThrows(UnsupportedOperationException.class, () ->
+ profile.getRequiredPeers().add("new-peer")
+ );
+
+ assertThrows(UnsupportedOperationException.class, () ->
+ profile.getRequiredCapabilities().add("new-capability")
+ );
+
+ assertThrows(UnsupportedOperationException.class, () ->
+ profile.getKeyToPeerMapping().put("new-key", "new-peer")
+ );
+ }
+}
diff --git a/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/autoconfigure/properties/RoleProfileTest.java b/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/autoconfigure/properties/RoleProfileTest.java
new file mode 100644
index 0000000..cf71395
--- /dev/null
+++ b/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/autoconfigure/properties/RoleProfileTest.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2026 Alibaba Group Holding Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.alibaba.openagentauth.spring.autoconfigure.properties;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Unit tests for {@link RoleProfile}.
+ *
+ * @since 2.1
+ */
+@SpringBootTest(classes = TestConfiguration.class)
+class RoleProfileTest {
+
+ @Test
+ void testBuilderPattern() {
+ RoleProfile profile = RoleProfile.builder()
+ .signingKeys("key1", "key2")
+ .verificationKeys("key3")
+ .encryptionKeys("key4")
+ .decryptionKeys("key5")
+ .requiredPeers("peer1", "peer2")
+ .requiredCapabilities("capability1")
+ .jwksProviderEnabled(false)
+ .keyDefaultAlgorithms(Map.of("key1", "ES256"))
+ .keyToPeerMapping(Map.of("key3", "peer1"))
+ .build();
+
+ assertEquals(List.of("key1", "key2"), profile.getSigningKeys());
+ assertEquals(List.of("key3"), profile.getVerificationKeys());
+ assertEquals(List.of("key4"), profile.getEncryptionKeys());
+ assertEquals(List.of("key5"), profile.getDecryptionKeys());
+ assertEquals(List.of("peer1", "peer2"), profile.getRequiredPeers());
+ assertEquals(List.of("capability1"), profile.getRequiredCapabilities());
+ assertEquals(false, profile.isJwksProviderEnabled());
+ }
+
+ @Test
+ void testImmutabilityReturnsUnmodifiableList() {
+ RoleProfile profile = RoleProfile.builder()
+ .signingKeys("key1", "key2")
+ .verificationKeys("key3")
+ .build();
+
+ List signingKeys = profile.getSigningKeys();
+ assertThrows(UnsupportedOperationException.class, () -> signingKeys.add("new-key"));
+
+ List verificationKeys = profile.getVerificationKeys();
+ assertThrows(UnsupportedOperationException.class, () -> verificationKeys.add("new-key"));
+
+ List encryptionKeys = profile.getEncryptionKeys();
+ assertThrows(UnsupportedOperationException.class, () -> encryptionKeys.add("new-key"));
+
+ List decryptionKeys = profile.getDecryptionKeys();
+ assertThrows(UnsupportedOperationException.class, () -> decryptionKeys.add("new-key"));
+
+ List requiredPeers = profile.getRequiredPeers();
+ assertThrows(UnsupportedOperationException.class, () -> requiredPeers.add("new-peer"));
+
+ List requiredCapabilities = profile.getRequiredCapabilities();
+ assertThrows(UnsupportedOperationException.class, () -> requiredCapabilities.add("new-capability"));
+ }
+
+ @Test
+ void testImmutabilityReturnsUnmodifiableMap() {
+ RoleProfile profile = RoleProfile.builder()
+ .keyDefaultAlgorithms(Map.of("key1", "ES256"))
+ .keyToPeerMapping(Map.of("key1", "peer1"))
+ .build();
+
+ Map keyDefaultAlgorithms = profile.getKeyToPeerMapping();
+ assertThrows(UnsupportedOperationException.class, () -> keyDefaultAlgorithms.put("new-key", "new-value"));
+
+ Map keyToPeerMapping = profile.getKeyToPeerMapping();
+ assertThrows(UnsupportedOperationException.class, () -> keyToPeerMapping.put("new-key", "new-peer"));
+ }
+
+ @Test
+ void testGetDefaultAlgorithm() {
+ Map algorithms = new HashMap<>();
+ algorithms.put("wit-signing", "ES256");
+ algorithms.put("par-jwt-signing", "RS256");
+
+ RoleProfile profile = RoleProfile.builder()
+ .keyDefaultAlgorithms(algorithms)
+ .build();
+
+ assertEquals("ES256", profile.getDefaultAlgorithm("wit-signing"));
+ assertEquals("RS256", profile.getDefaultAlgorithm("par-jwt-signing"));
+ assertNull(profile.getDefaultAlgorithm("unknown-key"));
+ }
+
+ @Test
+ void testGetPeerForKey() {
+ Map mapping = new HashMap<>();
+ mapping.put("wit-verification", "agent-idp");
+ mapping.put("id-token-verification", "agent-user-idp");
+
+ RoleProfile profile = RoleProfile.builder()
+ .keyToPeerMapping(mapping)
+ .build();
+
+ assertEquals("agent-idp", profile.getPeerForKey("wit-verification"));
+ assertEquals("agent-user-idp", profile.getPeerForKey("id-token-verification"));
+ assertNull(profile.getPeerForKey("local-key"));
+ }
+
+ @Test
+ void testAllFieldGetters() {
+ RoleProfile profile = RoleProfile.builder()
+ .signingKeys("sign-key1", "sign-key2")
+ .verificationKeys("verify-key")
+ .encryptionKeys("encrypt-key")
+ .decryptionKeys("decrypt-key")
+ .requiredPeers("peer1", "peer2")
+ .requiredCapabilities("cap1", "cap2")
+ .jwksProviderEnabled(true)
+ .keyDefaultAlgorithms(Map.of("key1", "ES256"))
+ .keyToPeerMapping(Map.of("key2", "peer1"))
+ .build();
+
+ assertEquals(List.of("sign-key1", "sign-key2"), profile.getSigningKeys());
+ assertEquals(List.of("verify-key"), profile.getVerificationKeys());
+ assertEquals(List.of("encrypt-key"), profile.getEncryptionKeys());
+ assertEquals(List.of("decrypt-key"), profile.getDecryptionKeys());
+ assertEquals(List.of("peer1", "peer2"), profile.getRequiredPeers());
+ assertEquals(List.of("cap1", "cap2"), profile.getRequiredCapabilities());
+ assertEquals(true, profile.isJwksProviderEnabled());
+ assertEquals("ES256", profile.getDefaultAlgorithm("key1"));
+ assertEquals("peer1", profile.getPeerForKey("key2"));
+ }
+
+ @Test
+ void testBuilderWithEmptyCollections() {
+ RoleProfile profile = RoleProfile.builder()
+ .build();
+
+ assertNotNull(profile.getSigningKeys());
+ assertTrue(profile.getSigningKeys().isEmpty());
+
+ assertNotNull(profile.getVerificationKeys());
+ assertTrue(profile.getVerificationKeys().isEmpty());
+
+ assertNotNull(profile.getEncryptionKeys());
+ assertTrue(profile.getEncryptionKeys().isEmpty());
+
+ assertNotNull(profile.getDecryptionKeys());
+ assertTrue(profile.getDecryptionKeys().isEmpty());
+
+ assertNotNull(profile.getRequiredPeers());
+ assertTrue(profile.getRequiredPeers().isEmpty());
+
+ assertNotNull(profile.getRequiredCapabilities());
+ assertTrue(profile.getRequiredCapabilities().isEmpty());
+
+ assertNotNull(profile.getKeyToPeerMapping());
+ assertTrue(profile.getKeyToPeerMapping().isEmpty());
+ }
+
+ @Test
+ void testBuilderDefaultValues() {
+ RoleProfile profile = RoleProfile.builder()
+ .build();
+
+ assertEquals(true, profile.isJwksProviderEnabled());
+ }
+
+ @Test
+ void testBuilderRequiresNonNullForMaps() {
+ assertThrows(NullPointerException.class, () ->
+ RoleProfile.builder()
+ .keyDefaultAlgorithms(null)
+ .build()
+ );
+
+ assertThrows(NullPointerException.class, () ->
+ RoleProfile.builder()
+ .keyToPeerMapping(null)
+ .build()
+ );
+ }
+}
diff --git a/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/autoconfigure/properties/RolesPropertiesTest.java b/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/autoconfigure/properties/RolesPropertiesTest.java
index 7342707..ce963fc 100644
--- a/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/autoconfigure/properties/RolesPropertiesTest.java
+++ b/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/autoconfigure/properties/RolesPropertiesTest.java
@@ -114,7 +114,6 @@ void testRolePropertiesDefaultValues() {
RolesProperties.RoleProperties role = new RolesProperties.RoleProperties();
assertFalse(role.isEnabled());
- assertNull(role.getInstanceId());
assertNull(role.getIssuer());
}
@@ -125,9 +124,6 @@ void testRolePropertiesGetterSetter() {
role.setEnabled(true);
assertTrue(role.isEnabled());
- role.setInstanceId("instance-001");
- assertEquals("instance-001", role.getInstanceId());
-
role.setIssuer("http://localhost:8080");
assertEquals("http://localhost:8080", role.getIssuer());
}
@@ -144,10 +140,8 @@ void testBoundaryValues() {
RolesProperties.RoleProperties role = new RolesProperties.RoleProperties();
role.setEnabled(true);
- role.setInstanceId("");
role.setIssuer("");
properties.putRole("test", role);
- assertEquals("", properties.getRole("test").getInstanceId());
assertEquals("", properties.getRole("test").getIssuer());
}
diff --git a/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/autoconfigure/role/AgentAutoConfigurationTest.java b/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/autoconfigure/role/AgentAutoConfigurationTest.java
index 124b787..5625408 100644
--- a/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/autoconfigure/role/AgentAutoConfigurationTest.java
+++ b/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/autoconfigure/role/AgentAutoConfigurationTest.java
@@ -1275,4 +1275,68 @@ public CustomAgentAapExecutor(
super(agent, vcSigner, policyBuilder, promptProtectionChain, config);
}
}
+
+ @Nested
+ @DisplayName("JweEncryptionKeyId Strategy Tests")
+ class JweEncryptionKeyIdStrategyTests {
+
+ @Test
+ @DisplayName("jweEncryptionKeyId should use key definition name (Strategy 1)")
+ void jweEncryptionKeyId_shouldUseKeyDefinitionName() {
+ contextRunner
+ .withUserConfiguration(WitKeyTestConfiguration.class)
+ .withPropertyValues(
+ "open-agent-auth.role=agent",
+ "open-agent-auth.issuer=http://localhost:8080",
+ "open-agent-auth.infrastructures.trust-domain=wimse://test.trust.domain",
+ "open-agent-auth.capabilities.operation-authorization.enabled=true",
+ "open-agent-auth.capabilities.oauth2-client.client-id=test-client-id",
+ "open-agent-auth.capabilities.oauth2-client.client-secret=test-client-secret",
+ "open-agent-auth.capabilities.oauth2-client.redirect-uri=http://localhost:8080/callback",
+ "open-agent-auth.capabilities.operation-authorization.agent-context.default-channel=test-channel",
+ "open-agent-auth.capabilities.operation-authorization.agent-context.default-language=en-US",
+ "open-agent-auth.capabilities.operation-authorization.agent-context.default-platform=test-platform",
+ "open-agent-auth.capabilities.operation-authorization.agent-context.default-client=test-agent-client",
+ "open-agent-auth.capabilities.operation-authorization.prompt-encryption.enabled=true",
+ "open-agent-auth.capabilities.operation-authorization.prompt-encryption.encryption-algorithm=RSA-OAEP-256",
+ "open-agent-auth.capabilities.operation-authorization.prompt-encryption.content-encryption-algorithm=A256GCM",
+ "open-agent-auth.infrastructures.key-management.keys.jwe-encryption.key-id=jwe-encryption-key-id",
+ "open-agent-auth.infrastructures.key-management.keys.jwe-encryption.algorithm=RS256"
+ )
+ .run(context -> {
+ assertThat(context).hasSingleBean(String.class);
+ assertThat(context).hasNotFailed();
+ });
+ }
+
+ @Test
+ @DisplayName("jweEncryptionKeyId should fallback to explicit encryption-key-id (Strategy 2)")
+ void jweEncryptionKeyId_shouldFallbackToExplicitKeyId() {
+ contextRunner
+ .withUserConfiguration(WitKeyTestConfiguration.class)
+ .withPropertyValues(
+ "open-agent-auth.role=agent",
+ "open-agent-auth.issuer=http://localhost:8080",
+ "open-agent-auth.infrastructures.trust-domain=wimse://test.trust.domain",
+ "open-agent-auth.capabilities.operation-authorization.enabled=true",
+ "open-agent-auth.capabilities.oauth2-client.client-id=test-client-id",
+ "open-agent-auth.capabilities.oauth2-client.client-secret=test-client-secret",
+ "open-agent-auth.capabilities.oauth2-client.redirect-uri=http://localhost:8080/callback",
+ "open-agent-auth.capabilities.operation-authorization.agent-context.default-channel=test-channel",
+ "open-agent-auth.capabilities.operation-authorization.agent-context.default-language=en-US",
+ "open-agent-auth.capabilities.operation-authorization.agent-context.default-platform=test-platform",
+ "open-agent-auth.capabilities.operation-authorization.agent-context.default-client=test-agent-client",
+ "open-agent-auth.capabilities.operation-authorization.prompt-encryption.enabled=true",
+ "open-agent-auth.capabilities.operation-authorization.prompt-encryption.encryption-key-id=explicit-encryption-key-id",
+ "open-agent-auth.capabilities.operation-authorization.prompt-encryption.encryption-algorithm=RSA-OAEP-256",
+ "open-agent-auth.capabilities.operation-authorization.prompt-encryption.content-encryption-algorithm=A256GCM",
+ "open-agent-auth.infrastructures.key-management.keys.jwe.key-id=explicit-encryption-key-id",
+ "open-agent-auth.infrastructures.key-management.keys.jwe.algorithm=RS256"
+ )
+ .run(context -> {
+ assertThat(context).hasSingleBean(String.class);
+ assertThat(context).hasNotFailed();
+ });
+ }
+ }
}
diff --git a/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/autoconfigure/role/JweEncryptionAutoConfigurationTest.java b/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/autoconfigure/role/JweEncryptionAutoConfigurationTest.java
index 9ee584f..01117dd 100644
--- a/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/autoconfigure/role/JweEncryptionAutoConfigurationTest.java
+++ b/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/autoconfigure/role/JweEncryptionAutoConfigurationTest.java
@@ -15,19 +15,16 @@
*/
package com.alibaba.openagentauth.spring.autoconfigure.role;
-import com.alibaba.openagentauth.core.crypto.jwe.JweDecoder;
-import com.alibaba.openagentauth.core.crypto.jwe.JweEncoder;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
import com.alibaba.openagentauth.core.crypto.jwe.NimbusJweDecoder;
import com.alibaba.openagentauth.core.crypto.jwe.NimbusJweEncoder;
import com.alibaba.openagentauth.core.crypto.key.DefaultKeyManager;
-import com.alibaba.openagentauth.core.crypto.key.KeyManager;
import com.alibaba.openagentauth.core.crypto.key.model.KeyAlgorithm;
import com.alibaba.openagentauth.core.crypto.key.store.InMemoryKeyStore;
import com.alibaba.openagentauth.core.exception.crypto.KeyManagementException;
-import com.alibaba.openagentauth.core.protocol.vc.jwe.PromptDecryptionService;
-import com.alibaba.openagentauth.core.protocol.vc.jwe.PromptEncryptionService;
-import com.alibaba.openagentauth.spring.autoconfigure.core.CoreAutoConfiguration;
-import com.alibaba.openagentauth.spring.autoconfigure.properties.OpenAgentAuthProperties;
import com.alibaba.openagentauth.spring.autoconfigure.properties.capabilities.OperationAuthorizationProperties;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
@@ -35,9 +32,20 @@
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+import org.springframework.core.type.AnnotatedTypeMetadata;
+import org.springframework.mock.env.MockEnvironment;
-import static org.assertj.core.api.Assertions.assertThat;
+import com.alibaba.openagentauth.core.crypto.jwe.JweDecoder;
+import com.alibaba.openagentauth.core.crypto.jwe.JweEncoder;
+import com.alibaba.openagentauth.core.crypto.key.KeyManager;
+import com.alibaba.openagentauth.core.protocol.vc.jwe.PromptDecryptionService;
+import com.alibaba.openagentauth.core.protocol.vc.jwe.PromptEncryptionService;
+import com.alibaba.openagentauth.spring.autoconfigure.ConfigConstants;
+import com.alibaba.openagentauth.spring.autoconfigure.core.CoreAutoConfiguration;
+import com.alibaba.openagentauth.spring.autoconfigure.properties.OpenAgentAuthProperties;
/**
* Unit tests for {@link JweEncryptionAutoConfiguration}.
@@ -540,4 +548,185 @@ void shouldFailWhenEncryptionKeyIdIsNotConfigured() {
});
}
}
+
+ @Nested
+ @DisplayName("Key Resolution Strategy Tests")
+ class KeyResolutionStrategyTests {
+
+ @Test
+ @DisplayName("resolveDecryptionKeyId should use key definition name (Strategy 1)")
+ void resolveDecryptionKeyId_shouldUseKeyDefinitionName() {
+ // Use test-key-id which matches the key generated by TestKeyManagerConfiguration
+ contextRunner
+ .withPropertyValues(
+ "open-agent-auth.enabled=true",
+ "open-agent-auth.infrastructures.trust-domain=wimse://test.trust.domain",
+ "open-agent-auth.capabilities.operation-authorization.prompt-encryption.enabled=true",
+ "open-agent-auth.capabilities.operation-authorization.prompt-encryption.encryption-algorithm=RSA-OAEP-256",
+ "open-agent-auth.capabilities.operation-authorization.prompt-encryption.content-encryption-algorithm=A256GCM",
+ "open-agent-auth.infrastructures.key-management.keys.jwe-decryption.key-id=test-key-id",
+ "open-agent-auth.infrastructures.key-management.keys.jwe-decryption.algorithm=RS256"
+ )
+ .run(context -> {
+ assertThat(context).hasSingleBean(JweDecoder.class);
+ assertThat(context).hasNotFailed();
+ });
+ }
+
+ @Test
+ @DisplayName("resolveDecryptionKeyId should fallback to explicit encryption-key-id (Strategy 2)")
+ void resolveDecryptionKeyId_shouldFallbackToExplicitKeyId() {
+ contextRunner
+ .withPropertyValues(
+ "open-agent-auth.enabled=true",
+ "open-agent-auth.infrastructures.trust-domain=wimse://test.trust.domain",
+ "open-agent-auth.capabilities.operation-authorization.prompt-encryption.enabled=true",
+ "open-agent-auth.capabilities.operation-authorization.prompt-encryption.encryption-key-id=test-key-id",
+ "open-agent-auth.capabilities.operation-authorization.prompt-encryption.encryption-algorithm=RSA-OAEP-256",
+ "open-agent-auth.capabilities.operation-authorization.prompt-encryption.content-encryption-algorithm=A256GCM"
+ )
+ .run(context -> {
+ assertThat(context).hasSingleBean(JweDecoder.class);
+ assertThat(context).hasNotFailed();
+ });
+ }
+
+ @Test
+ @DisplayName("resolveEncryptionKey should use local decryption key for AS role (Strategy 2)")
+ void resolveEncryptionKey_shouldUseLocalDecryptionKey() {
+ // Use test-key-id which matches the key generated by TestKeyManagerConfiguration
+ contextRunner
+ .withPropertyValues(
+ "open-agent-auth.enabled=true",
+ "open-agent-auth.infrastructures.trust-domain=wimse://test.trust.domain",
+ "open-agent-auth.capabilities.operation-authorization.prompt-encryption.enabled=true",
+ "open-agent-auth.capabilities.operation-authorization.prompt-encryption.encryption-algorithm=RSA-OAEP-256",
+ "open-agent-auth.capabilities.operation-authorization.prompt-encryption.content-encryption-algorithm=A256GCM",
+ "open-agent-auth.infrastructures.key-management.keys.jwe-decryption.key-id=test-key-id",
+ "open-agent-auth.infrastructures.key-management.keys.jwe-decryption.algorithm=RS256"
+ )
+ .run(context -> {
+ assertThat(context).hasSingleBean(JweEncoder.class);
+ assertThat(context).hasNotFailed();
+ });
+ }
+ }
+
+ @Nested
+ @DisplayName("DecryptionKeyAvailableCondition Tests")
+ class DecryptionKeyAvailableConditionTests {
+
+ private final JweEncryptionAutoConfiguration.DecryptionKeyAvailableCondition condition =
+ new JweEncryptionAutoConfiguration.DecryptionKeyAvailableCondition();
+
+ @Test
+ @DisplayName("Should match when jwe-decryption.key-id exists")
+ void shouldMatchWhenJweDecryptionKeyIdExists() {
+ MockEnvironment environment = new MockEnvironment();
+ environment.setProperty(
+ "open-agent-auth.infrastructures.key-management.keys.jwe-decryption.key-id",
+ "decryption-key-id"
+ );
+
+ ConditionContext context = mock(ConditionContext.class);
+ when(context.getEnvironment()).thenReturn(environment);
+
+ AnnotatedTypeMetadata metadata = mock(AnnotatedTypeMetadata.class);
+
+ boolean result = condition.matches(context, metadata);
+
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ @DisplayName("Should match when explicit encryption-key-id exists")
+ void shouldMatchWhenExplicitEncryptionKeyIdExists() {
+ MockEnvironment environment = new MockEnvironment();
+ environment.setProperty(
+ "open-agent-auth.capabilities.operation-authorization.prompt-encryption.encryption-key-id",
+ "explicit-key-id"
+ );
+
+ ConditionContext context = mock(ConditionContext.class);
+ when(context.getEnvironment()).thenReturn(environment);
+
+ AnnotatedTypeMetadata metadata = mock(AnnotatedTypeMetadata.class);
+
+ boolean result = condition.matches(context, metadata);
+
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ @DisplayName("Should not match when no decryption key available")
+ void shouldNotMatchWhenNoDecryptionKeyAvailable() {
+ MockEnvironment environment = new MockEnvironment();
+
+ ConditionContext context = mock(ConditionContext.class);
+ when(context.getEnvironment()).thenReturn(environment);
+
+ AnnotatedTypeMetadata metadata = mock(AnnotatedTypeMetadata.class);
+
+ boolean result = condition.matches(context, metadata);
+
+ assertThat(result).isFalse();
+ }
+
+ @Test
+ @DisplayName("Should not match when keys are blank")
+ void shouldNotMatchWhenKeysAreBlank() {
+ MockEnvironment environment = new MockEnvironment();
+ environment.setProperty(
+ "open-agent-auth.infrastructures.key-management.keys.jwe-decryption.key-id",
+ ""
+ );
+ environment.setProperty(
+ "open-agent-auth.capabilities.operation-authorization.prompt-encryption.encryption-key-id",
+ " "
+ );
+
+ ConditionContext context = mock(ConditionContext.class);
+ when(context.getEnvironment()).thenReturn(environment);
+
+ AnnotatedTypeMetadata metadata = mock(AnnotatedTypeMetadata.class);
+
+ boolean result = condition.matches(context, metadata);
+
+ assertThat(result).isFalse();
+ }
+
+ @Test
+ @DisplayName("Should match when authorization-server role is enabled (Strategy 3 — inferred decryption key)")
+ void shouldMatchWhenAuthorizationServerRoleEnabled() {
+ MockEnvironment environment = new MockEnvironment();
+ // No explicit jwe-decryption key or encryption-key-id, but AS role is enabled
+ environment.setProperty("open-agent-auth.roles.authorization-server.enabled", "true");
+
+ ConditionContext context = mock(ConditionContext.class);
+ when(context.getEnvironment()).thenReturn(environment);
+
+ AnnotatedTypeMetadata metadata = mock(AnnotatedTypeMetadata.class);
+
+ boolean result = condition.matches(context, metadata);
+
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ @DisplayName("Should not match when agent role is enabled (no decryption keys in profile)")
+ void shouldNotMatchWhenAgentRoleEnabled() {
+ MockEnvironment environment = new MockEnvironment();
+ // Agent role has no decryptionKeys in its profile
+ environment.setProperty("open-agent-auth.roles.agent.enabled", "true");
+
+ ConditionContext context = mock(ConditionContext.class);
+ when(context.getEnvironment()).thenReturn(environment);
+
+ AnnotatedTypeMetadata metadata = mock(AnnotatedTypeMetadata.class);
+
+ boolean result = condition.matches(context, metadata);
+
+ assertThat(result).isFalse();
+ }
+ }
}
\ No newline at end of file
diff --git a/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/web/model/OaaConfigurationMetadataTest.java b/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/web/model/OaaConfigurationMetadataTest.java
new file mode 100644
index 0000000..5aee7cd
--- /dev/null
+++ b/open-agent-auth-spring-boot-starter/src/test/java/com/alibaba/openagentauth/spring/web/model/OaaConfigurationMetadataTest.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright 2026 Alibaba Group Holding Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.alibaba.openagentauth.spring.web.model;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/**
+ * Unit tests for {@link OaaConfigurationMetadata}.
+ *
+ * @since 2.1
+ */
+class OaaConfigurationMetadataTest {
+
+ @Test
+ void testCurrentProtocolVersion() {
+ assertEquals("1.0", OaaConfigurationMetadata.CURRENT_PROTOCOL_VERSION);
+ }
+
+ @Test
+ void testDefaultValues() {
+ OaaConfigurationMetadata metadata = new OaaConfigurationMetadata();
+
+ assertNull(metadata.getIssuer());
+ assertNull(metadata.getRoles());
+ assertNull(metadata.getTrustDomain());
+ assertEquals("1.0", metadata.getProtocolVersion());
+ assertNull(metadata.getJwksUri());
+ assertNull(metadata.getSigningAlgorithmsSupported());
+ assertNull(metadata.getCapabilities());
+ assertNull(metadata.getEndpoints());
+ assertNull(metadata.getPeersRequired());
+ }
+
+ @Test
+ void testIssuerGetterSetter() {
+ OaaConfigurationMetadata metadata = new OaaConfigurationMetadata();
+
+ metadata.setIssuer("http://localhost:8080");
+ assertEquals("http://localhost:8080", metadata.getIssuer());
+ }
+
+ @Test
+ void testRolesGetterSetter() {
+ OaaConfigurationMetadata metadata = new OaaConfigurationMetadata();
+
+ List roles = List.of("agent", "agent-idp");
+ metadata.setRoles(roles);
+ assertEquals(roles, metadata.getRoles());
+ }
+
+ @Test
+ void testTrustDomainGetterSetter() {
+ OaaConfigurationMetadata metadata = new OaaConfigurationMetadata();
+
+ metadata.setTrustDomain("wimse://default.trust.domain");
+ assertEquals("wimse://default.trust.domain", metadata.getTrustDomain());
+ }
+
+ @Test
+ void testProtocolVersionGetterSetter() {
+ OaaConfigurationMetadata metadata = new OaaConfigurationMetadata();
+
+ metadata.setProtocolVersion("2.0");
+ assertEquals("2.0", metadata.getProtocolVersion());
+ }
+
+ @Test
+ void testJwksUriGetterSetter() {
+ OaaConfigurationMetadata metadata = new OaaConfigurationMetadata();
+
+ metadata.setJwksUri("http://localhost:8080/.well-known/jwks.json");
+ assertEquals("http://localhost:8080/.well-known/jwks.json", metadata.getJwksUri());
+ }
+
+ @Test
+ void testSigningAlgorithmsSupportedGetterSetter() {
+ OaaConfigurationMetadata metadata = new OaaConfigurationMetadata();
+
+ List algorithms = List.of("RS256", "ES256");
+ metadata.setSigningAlgorithmsSupported(algorithms);
+ assertEquals(algorithms, metadata.getSigningAlgorithmsSupported());
+ }
+
+ @Test
+ void testCapabilitiesGetterSetter() {
+ OaaConfigurationMetadata metadata = new OaaConfigurationMetadata();
+
+ Map capabilities = new HashMap<>();
+ capabilities.put("oauth2", true);
+ capabilities.put("workload-identity", true);
+ metadata.setCapabilities(capabilities);
+ assertEquals(capabilities, metadata.getCapabilities());
+ }
+
+ @Test
+ void testEndpointsGetterSetter() {
+ OaaConfigurationMetadata metadata = new OaaConfigurationMetadata();
+
+ Map endpoints = new HashMap<>();
+ endpoints.put("oauth2.authorize", "http://localhost:8080/oauth2/authorize");
+ endpoints.put("oauth2.token", "http://localhost:8080/oauth2/token");
+ metadata.setEndpoints(endpoints);
+ assertEquals(endpoints, metadata.getEndpoints());
+ }
+
+ @Test
+ void testPeersRequiredGetterSetter() {
+ OaaConfigurationMetadata metadata = new OaaConfigurationMetadata();
+
+ List peers = List.of("agent-idp", "authorization-server");
+ metadata.setPeersRequired(peers);
+ assertEquals(peers, metadata.getPeersRequired());
+ }
+
+ @Test
+ void testAllFieldsPopulated() {
+ OaaConfigurationMetadata metadata = new OaaConfigurationMetadata();
+
+ metadata.setIssuer("http://localhost:8080");
+ metadata.setRoles(List.of("agent", "agent-idp"));
+ metadata.setTrustDomain("wimse://default.trust.domain");
+ metadata.setProtocolVersion("1.0");
+ metadata.setJwksUri("http://localhost:8080/.well-known/jwks.json");
+ metadata.setSigningAlgorithmsSupported(List.of("RS256", "ES256"));
+
+ Map capabilities = new HashMap<>();
+ capabilities.put("oauth2", true);
+ metadata.setCapabilities(capabilities);
+
+ Map endpoints = new HashMap<>();
+ endpoints.put("oauth2.authorize", "http://localhost:8080/oauth2/authorize");
+ metadata.setEndpoints(endpoints);
+
+ metadata.setPeersRequired(List.of("agent-idp"));
+
+ assertNotNull(metadata.getIssuer());
+ assertNotNull(metadata.getRoles());
+ assertNotNull(metadata.getTrustDomain());
+ assertNotNull(metadata.getProtocolVersion());
+ assertNotNull(metadata.getJwksUri());
+ assertNotNull(metadata.getSigningAlgorithmsSupported());
+ assertNotNull(metadata.getCapabilities());
+ assertNotNull(metadata.getEndpoints());
+ assertNotNull(metadata.getPeersRequired());
+ }
+
+ @Test
+ void testProtocolVersionDefault() {
+ OaaConfigurationMetadata metadata = new OaaConfigurationMetadata();
+ assertEquals("1.0", metadata.getProtocolVersion());
+ }
+
+ @Test
+ void testEmptyCollections() {
+ OaaConfigurationMetadata metadata = new OaaConfigurationMetadata();
+
+ metadata.setRoles(List.of());
+ assertNotNull(metadata.getRoles());
+ assertEquals(0, metadata.getRoles().size());
+
+ metadata.setSigningAlgorithmsSupported(List.of());
+ assertNotNull(metadata.getSigningAlgorithmsSupported());
+ assertEquals(0, metadata.getSigningAlgorithmsSupported().size());
+
+ metadata.setCapabilities(new HashMap<>());
+ assertNotNull(metadata.getCapabilities());
+ assertEquals(0, metadata.getCapabilities().size());
+
+ metadata.setEndpoints(new HashMap<>());
+ assertNotNull(metadata.getEndpoints());
+ assertEquals(0, metadata.getEndpoints().size());
+
+ metadata.setPeersRequired(List.of());
+ assertNotNull(metadata.getPeersRequired());
+ assertEquals(0, metadata.getPeersRequired().size());
+ }
+}