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-[^)]*)|![Code Coverage](https://img.shields.io/badge/coverage-${COVERAGE}%25-${COLOR})|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 @@ ![Java](https://img.shields.io/badge/Java-17+-orange.svg) ![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.3+-green.svg) ![Code Coverage](https://img.shields.io/badge/coverage-82%25-brightgreen) - ![Build Status](https://img.shields.io/badge/build-passing-brightgreen) + ![Build Status](https://github.com/idem/idem-agent-auth/actions/workflows/ci.yml/badge.svg) ![Version](https://img.shields.io/badge/version-v0.1.0--beta.1-blue) [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 @@ ![Java](https://img.shields.io/badge/Java-17+-orange.svg) ![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.3+-green.svg) ![Code Coverage](https://img.shields.io/badge/coverage-82%25-brightgreen) - ![Build Status](https://img.shields.io/badge/build-passing-brightgreen) + ![Build Status](https://github.com/idem/idem-agent-auth/actions/workflows/ci.yml/badge.svg) ![Version](https://img.shields.io/badge/version-v0.1.0--beta.1-blue) [快速开始](#快速开始) · [架构](#架构) · [安全机制](#安全机制) · [文档资源](#文档资源) · [路线图](#路线图) 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:

+ *
    + *
  1. Peer expansion: Each declared peer is expanded into a JWKS consumer + * and a service-discovery entry
  2. + *
  3. Key inference: Based on the enabled role's profile, required signing keys, + * verification keys, encryption keys, and decryption keys are automatically configured
  4. + *
  5. Provider defaults: If no key providers are configured, an in-memory + * provider is automatically added
  6. + *
  7. JWKS provider: Automatically enabled if the role profile requires it
  8. + *
+ * + *

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: + *
    + *
  1. 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.
  2. + *
  3. 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.
  4. + *
  5. Fall back to the explicit {@code encryption-key-id} from capabilities configuration, + * which supports legacy explicit key configuration.
  6. + *
+ */ + 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: + *

    + *
  1. A {@code jwe-decryption} key definition exists explicitly in the YAML configuration.
  2. + *
  3. An explicit {@code encryption-key-id} is set in the prompt-encryption configuration.
  4. + *
  5. 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.
  6. + *
+ * 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()); + } +}