feat(mcp): add OAuth 2.1 authorization with JWT validation and RFC 9728 metadata #9818
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Router CI | |
| on: | |
| pull_request: | |
| paths: | |
| - 'pnpm-lock.yaml' | |
| - 'composition-go/**/*' | |
| - 'demo/**/*' | |
| - 'router/**/*' | |
| - 'router-plugin/**/*' | |
| - 'router-tests/**/*' | |
| - 'connect/**/*' | |
| - '.github/workflows/router-ci.yaml' | |
| concurrency: | |
| group: ${{github.workflow}}-${{github.head_ref}} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read # for actions/checkout to fetch code | |
| pull-requests: write # required for adding pull request comments | |
| packages: write # required for publishing packages | |
| env: | |
| CI: true | |
| ROUTER_REGISTRATION: false | |
| DO_NOT_TRACK: '1' | |
| # Both jobs need to kept in sync. We need to distinguish between forks and people with write access to the repository. | |
| jobs: | |
| # Runs for forks without access to repository secrets | |
| build_test_fork: | |
| if: github.event.pull_request.head.repo.full_name != github.repository | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/go | |
| with: | |
| cache-dependency-path: | | |
| router/go.sum | |
| router-tests/go.sum | |
| demo/go.sum | |
| - uses: ./.github/actions/go-mod-tidy | |
| with: | |
| working-directory: ./router | |
| - name: Install tools | |
| run: make setup-build-tools | |
| - uses: ./.github/actions/node | |
| - name: Install Cli Node Dependencies | |
| run: | | |
| pnpm --filter ./cli --filter ./connect --filter ./shared --filter ./composition --filter ./protographic install --frozen-lockfile | |
| pnpm buf generate --template buf.ts.gen.yaml | |
| pnpm --filter ./cli --filter ./connect --filter ./shared --filter ./composition --filter ./protographic run build | |
| - name: Install Bun For Plugin Building | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: 1.2.15 | |
| - name: Generate code | |
| run: make generate-go | |
| - name: Check if git is not dirty after generating files | |
| run: git diff --no-ext-diff --exit-code | |
| - name: Install dependencies | |
| working-directory: ./router | |
| run: go mod download | |
| - name: Run linters on router-tests | |
| uses: ./.github/actions/go-linter | |
| with: | |
| working-directory: ./router-tests | |
| - name: Run linters on router | |
| uses: ./.github/actions/go-linter | |
| with: | |
| working-directory: ./router | |
| - name: Test | |
| working-directory: ./router | |
| run: make test | |
| - name: Build | |
| working-directory: ./router | |
| run: make build | |
| # Runs for forks to test docker image is building | |
| build_image_fork: | |
| if: github.event.pull_request.head.repo.full_name != github.repository | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| strategy: | |
| matrix: | |
| docker_build_target: [ '', 'nonroot' ] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/build-push-image | |
| with: | |
| docker_context: router | |
| dockerfile: router/Dockerfile | |
| docker_build_target: ${{ matrix.docker_build_target }} | |
| token: ${{secrets.GITHUB_TOKEN}} | |
| image_name: router | |
| image_description: 'Cosmo Router' | |
| push: 'false' | |
| # Runs for people with write access to the repository | |
| # that have access to the secrets | |
| build_test: | |
| if: github.event.pull_request.head.repo.full_name == github.repository | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Log in to Docker Container registry (With write access) | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: docker.io | |
| username: ${{secrets.DOCKER_USERNAME}} | |
| password: ${{secrets.DOCKER_PASSWORD}} | |
| - uses: ./.github/actions/go | |
| with: | |
| cache-dependency-path: | | |
| router/go.sum | |
| router-tests/go.sum | |
| demo/go.sum | |
| - uses: ./.github/actions/go-mod-tidy | |
| with: | |
| working-directory: ./router | |
| - name: Install tools | |
| run: make setup-build-tools | |
| - uses: ./.github/actions/node | |
| - name: Install Cli Node Dependencies | |
| run: | | |
| pnpm --filter ./cli --filter ./connect --filter ./shared --filter ./composition --filter ./protographic install --frozen-lockfile | |
| pnpm buf generate --template buf.ts.gen.yaml | |
| pnpm --filter ./cli --filter ./connect --filter ./shared --filter ./composition --filter ./protographic run build | |
| - name: Install Bun For Plugin Building | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: 1.2.15 | |
| - name: Generate code | |
| run: make generate-go | |
| - name: Check if git is not dirty after generating files | |
| run: git diff --no-ext-diff --exit-code | |
| - name: Install dependencies | |
| working-directory: ./router | |
| run: go mod download | |
| - name: Run linters on router-tests | |
| uses: ./.github/actions/go-linter | |
| with: | |
| working-directory: ./router-tests | |
| - name: Run linters on router | |
| uses: ./.github/actions/go-linter | |
| with: | |
| working-directory: ./router | |
| - name: Test | |
| working-directory: ./router | |
| run: make test-coverage | |
| - name: Build | |
| working-directory: ./router | |
| run: make build | |
| - name: Upload integration results to Codecov | |
| uses: ./.github/actions/codecov-upload-pr | |
| with: | |
| artifact-name: router-tests-coverage | |
| coverage-path: router/coverage.out | |
| retention-days: 14 | |
| codecov-token: ${{ secrets.CODECOV_TOKEN }} | |
| integration_test: | |
| runs-on: ubuntu-latest-l | |
| timeout-minutes: 30 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| test_target: | |
| [ | |
| './. ./fuzzquery ./lifecycle ./modules', | |
| './telemetry', | |
| './events', | |
| ] | |
| services: | |
| nats: | |
| image: ghcr.io/wundergraph/cosmo/nats:2.11.0-alpine | |
| options: >- | |
| --health-cmd "curl -f http://localhost:8222/healthz" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| ports: | |
| - 4222:4222 | |
| - 8222:8222 | |
| - 6222:6222 | |
| redis: | |
| image: redis:7 | |
| # Set health checks to wait until redis has started | |
| options: >- | |
| --health-cmd "redis-cli ping" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| ports: | |
| - 6379:6379 | |
| redis-0: | |
| image: bitnamilegacy/redis-cluster:7.2 | |
| options: >- | |
| --health-cmd "redis-cli ping" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| env: | |
| ALLOW_EMPTY_PASSWORD: "yes" | |
| REDIS_NODES: "redis-0 redis-1 redis-2" | |
| REDIS_CLUSTER_REPLICAS: "0" | |
| REDIS_CLUSTER_CREATOR: "no" | |
| ports: | |
| - 7001:6379 | |
| redis-1: | |
| image: bitnamilegacy/redis-cluster:7.2 | |
| options: >- | |
| --health-cmd "redis-cli ping" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| env: | |
| ALLOW_EMPTY_PASSWORD: "yes" | |
| REDIS_NODES: "redis-0 redis-1 redis-2" | |
| REDIS_CLUSTER_REPLICAS: "0" | |
| REDIS_CLUSTER_CREATOR: "no" | |
| ports: | |
| - 7002:6379 | |
| redis-2: | |
| image: bitnamilegacy/redis-cluster:7.2 | |
| options: >- | |
| --health-cmd "redis-cli ping" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| env: | |
| ALLOW_EMPTY_PASSWORD: "yes" | |
| REDIS_NODES: "redis-0 redis-1 redis-2" | |
| REDIS_CLUSTER_REPLICAS: "0" | |
| REDIS_CLUSTER_CREATOR: "yes" | |
| ports: | |
| - 7003:6379 | |
| kafka: | |
| image: bitnamilegacy/kafka:3.7.0 | |
| options: >- | |
| --health-cmd "kafka-broker-api-versions.sh --version" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| env: | |
| KAFKA_ENABLE_KRAFT: yes | |
| KAFKA_CFG_PROCESS_ROLES: controller,broker | |
| KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER | |
| KAFKA_CFG_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093 | |
| KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT | |
| KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 1@localhost:9093 | |
| KAFKA_CFG_TRANSACTION_PARTITION_VERIFICATION_ENABLE: false | |
| KAFKA_CFG_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092 | |
| KAFKA_CFG_NODE_ID: 1 | |
| ALLOW_PLAINTEXT_LISTENER: yes | |
| KAFKA_KRAFT_CLUSTER_ID: XkpGZQ27R3eTl3OdTm2LYA # 16 byte base64-encoded UUID | |
| ports: | |
| - '9092:9092' | |
| steps: | |
| - name: Wait for Redis Cluster | |
| run: | | |
| echo "[CHECK] Waiting for Redis Cluster to become healthy..." | |
| cluster_containers=$(docker ps --quiet --filter "ancestor=bitnamilegacy/redis-cluster:7.2") | |
| success=0 | |
| for i in {1..30}; do | |
| if [ $i -eq 1 ]; then | |
| echo "[INIT] Forcing cluster creation..." | |
| # pick one container as the "creator" | |
| creator=$(echo $cluster_containers | awk '{print $1}') | |
| # run the cluster create command inside it | |
| docker exec "$creator" redis-cli --cluster create redis-0:6379 redis-1:6379 redis-2:6379 --cluster-replicas 0 --cluster-yes || true | |
| fi | |
| for cid in $cluster_containers; do | |
| docker exec "$cid" redis-cli -p 6379 cluster info | |
| if docker exec "$cid" redis-cli -p 6379 cluster info 2>/dev/null | grep -q "cluster_state:ok"; then | |
| echo "[SUCCESS] Redis Cluster is ready (reported by $cid)" | |
| success=1 | |
| break 2 | |
| fi | |
| done | |
| echo "[WAITING] Cluster not ready yet (attempt $i)..." | |
| sleep 2 | |
| done | |
| if [ $success -eq 0 ]; then | |
| echo "[ERROR] Redis Cluster did not become healthy in time" | |
| for cid in $cluster_containers; do | |
| echo "--- Cluster info for $cid ---" | |
| docker exec "$cid" redis-cli -p 6379 cluster info || true | |
| docker exec "$cid" redis-cli -p 6379 cluster nodes || true | |
| echo "--- Logs for $cid ---" | |
| docker logs "$cid" | tail -n 100 | |
| done | |
| exit 1 | |
| fi | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/go | |
| with: | |
| cache-dependency-path: | | |
| router-tests/go.sum | |
| - name: Install tools | |
| run: make setup-build-tools | |
| - uses: ./.github/actions/node | |
| - name: Install Cli Node Dependencies | |
| run: | | |
| pnpm --filter ./cli --filter ./connect --filter ./shared --filter ./composition --filter ./protographic install --frozen-lockfile | |
| pnpm buf generate --template buf.ts.gen.yaml | |
| pnpm --filter ./cli --filter ./connect --filter ./shared --filter ./composition --filter ./protographic run build | |
| - name: Install Bun For Plugin Building | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: 1.2.15 | |
| - name: Install dependencies | |
| working-directory: ./router-tests | |
| run: go mod download | |
| - name: Configure Redis Authentication & ACL | |
| run: | | |
| docker ps -a | |
| # Set a password for each master node | |
| for cid in $(docker ps --format "{{.ID}} {{.Image}}" | grep "redis-cluster" | awk '{print $1}'); do | |
| echo "Configuring ACLs in container $cid" | |
| docker exec "$cid" redis-cli -p 6379 ACL SETUSER cosmo on ">test" "~*" "+@all" | |
| docker exec "$cid" redis-cli -p 6379 ping | |
| done | |
| cid=$(docker ps --format "{{.ID}} {{.Image}}" | grep "redis:7" | awk '{print $1}') | |
| # Sanity checks | |
| docker exec "$cid" redis-cli -p 6379 ping | |
| docker exec "$cid" redis-cli -u "redis://cosmo:test@redis-0:6379" ping | |
| docker exec "$cid" redis-cli -u "redis://cosmo:test@redis-0:6379" cluster nodes | |
| - name: Run Integration tests ${{ matrix.test_target }} | |
| working-directory: ./router-tests | |
| run: make test-coverage test_retry_count=0 test_params="-run '^Test[^(Flaky)]' --timeout=5m -p 1 --parallel 10" test_target="${{ matrix.test_target }}" | |
| - name: Compute artifact name for codecov upload | |
| id: artifact_name | |
| run: | | |
| # Sanitize test target: remove './', replace spaces and special chars with underscores | |
| target="${{ matrix.test_target }}" | |
| sanitized=$(echo "$target" | sed 's|\.\/||g' | sed 's|[^a-zA-Z0-9]|_|g' | sed 's|_\+|_|g' | sed 's|^_||' | sed 's|_$||') | |
| echo "sanitized=$sanitized" >> $GITHUB_OUTPUT | |
| - name: Upload integration results to Codecov | |
| uses: ./.github/actions/codecov-upload-pr | |
| with: | |
| artifact-name: router-${{ steps.artifact_name.outputs.sanitized }}-nonFlaky | |
| coverage-path: router-tests/coverage.out | |
| retention-days: 14 | |
| codecov-token: ${{ secrets.CODECOV_TOKEN }} | |
| - name: Run Flaky Integration tests ${{ matrix.test_target }} | |
| working-directory: ./router-tests | |
| run: make test-coverage test_retry_count=3 test_params="-run '^TestFlaky' --timeout=5m -p 1 --parallel 10" test_target="${{ matrix.test_target }}" | |
| - name: Upload flaky integration results to Codecov | |
| uses: ./.github/actions/codecov-upload-pr | |
| with: | |
| artifact-name: router-${{ steps.artifact_name.outputs.sanitized }}-flaky | |
| coverage-path: router-tests/coverage.out | |
| retention-days: 14 | |
| codecov-token: ${{ secrets.CODECOV_TOKEN }} | |
| image_scan: | |
| if: github.event.pull_request.head.repo.full_name == github.repository | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| strategy: | |
| matrix: | |
| docker_build_target: [ '', 'nonroot' ] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/build-push-image | |
| id: docker_build | |
| with: | |
| docker_username: ${{secrets.DOCKER_USERNAME}} | |
| docker_password: ${{secrets.DOCKER_PASSWORD}} | |
| docker_context: router | |
| dockerfile: router/Dockerfile | |
| docker_build_target: ${{ matrix.docker_build_target }} | |
| token: ${{secrets.GITHUB_TOKEN}} | |
| image_name: router | |
| image_description: 'Cosmo Router' | |
| image_platforms: 'linux/amd64' | |
| load_Image: 'true' | |
| push: 'false' | |
| - uses: ./.github/actions/image-scan | |
| with: | |
| name: ${{ matrix.docker_build_target != '' && format('Router-{0}', matrix.docker_build_target) || 'Router' }} | |
| github_token: ${{secrets.GITHUB_TOKEN}} | |
| image_ref: ${{ steps.docker_build.outputs.image_ref }}${{ matrix.docker_build_target != '' && format('-{0}', matrix.docker_build_target) || '' }} | |
| build_push_image: | |
| # This is a limitation of GitHub. Only organization members can push to GitHub Container Registry | |
| # For now, we will disable the push to the GitHub Container Registry for external contributors | |
| if: github.event.pull_request.head.repo.full_name == github.repository | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| strategy: | |
| matrix: | |
| docker_build_target: [ '', 'nonroot' ] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/build-push-image | |
| with: | |
| docker_username: ${{secrets.DOCKER_USERNAME}} | |
| docker_password: ${{secrets.DOCKER_PASSWORD}} | |
| docker_context: router | |
| dockerfile: router/Dockerfile | |
| docker_build_target: ${{ matrix.docker_build_target }} | |
| token: ${{secrets.GITHUB_TOKEN}} | |
| image_name: router | |
| image_description: 'Cosmo Router' |