diff --git a/.github/scripts/release.sh b/.github/scripts/release.sh deleted file mode 100644 index 2a1f6a9..0000000 --- a/.github/scripts/release.sh +++ /dev/null @@ -1,124 +0,0 @@ -#!/bin/bash - -# Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). -# -# This software is the property of WSO2 LLC. and its suppliers, if any. -# Dissemination of any information or reproduction of any material contained -# herein in any form is strictly forbidden, unless permitted by WSO2 expressly. -# You may not alter or remove any copyright or other notice from copies of this content. -# - -# Exit the script on any command with non-zero exit status. -set -e -set -o pipefail - -UPSTREAM_BRANCH="main" - -# Assign command line arguments to variables. -GIT_TOKEN=$1 -WORK_DIR=$2 -VERSION_TYPE=$3 # possible values: major, minor, patch - -# Check if GIT_TOKEN is empty -if [ -z "$GIT_TOKEN" ]; then - echo "❌ Error: GIT_TOKEN is not set." - exit 1 -fi - -# Check if WORK_DIR is empty -if [ -z "$WORK_DIR" ]; then - echo "❌ Error: WORK_DIR is not set." - exit 1 -fi - -# Validate VERSION_TYPE -if [[ "$VERSION_TYPE" != "major" && "$VERSION_TYPE" != "minor" && "$VERSION_TYPE" != "patch" ]]; then - echo "❌ Error: VERSION_TYPE must be one of: major, minor, or patch." - exit 1 -fi - -BUILD_DIRECTORY="$WORK_DIR/build" -RELEASE_DIRECTORY="$BUILD_DIRECTORY/releases" - -# Navigate to the working directory. -cd "${WORK_DIR}" - -# Create the release directory. -if [ ! -d "$RELEASE_DIRECTORY" ]; then - mkdir -p "$RELEASE_DIRECTORY" -else - rm -rf "$RELEASE_DIRECTORY"/* -fi - -# Extract current version. -CURRENT_VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "0.0.0") -IFS='.' read -r MAJOR MINOR PATCH <<< "${CURRENT_VERSION}" - -# Determine which part to increment -case "$VERSION_TYPE" in - major) - MAJOR=$((MAJOR + 1)) - MINOR=0 - PATCH=0 - ;; - minor) - MINOR=$((MINOR + 1)) - PATCH=0 - ;; - patch|*) - PATCH=$((PATCH + 1)) - ;; -esac - -NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}" - -echo "Creating release packages for version $NEW_VERSION..." - -# List of supported OSes. -oses=("linux" "linux-arm" "darwin") - -# Navigate to the release directory. -cd "${RELEASE_DIRECTORY}" - -for os in "${oses[@]}"; do - os_dir="../$os" - - if [ -d "$os_dir" ]; then - release_artifact_folder="openmcpauthproxy_${os}-v${NEW_VERSION}" - mkdir -p "$release_artifact_folder" - - cp -r $os_dir/* "$release_artifact_folder" - - # Zip the release package. - zip_file="$release_artifact_folder.zip" - echo "Creating $zip_file..." - zip -r "$zip_file" "$release_artifact_folder" - - # Delete the folder after zipping. - rm -rf "$release_artifact_folder" - - # Generate checksum file. - sha256sum "$zip_file" | sed "s|target/releases/||" > "$zip_file.sha256" - echo "Checksum generated for the $os package." - - echo "Release packages created successfully for $os." - else - echo "Skipping $os release package creation as the build artifacts are not available." - fi -done - -echo "Release packages created successfully in $RELEASE_DIRECTORY." - -# Navigate back to the project root directory. -cd "${WORK_DIR}" - -# Collect all ZIP and .sha256 files in the target/releases directory. -FILES_TO_UPLOAD=$(find build/releases -type f \( -name "*.zip" -o -name "*.sha256" \)) - -# Create a release with the current version. -TAG_NAME="v${NEW_VERSION}" -export GITHUB_TOKEN="${GIT_TOKEN}" -gh release create "${TAG_NAME}" ${FILES_TO_UPLOAD} --title "${TAG_NAME}" --notes "OpenMCPAuthProxy - ${TAG_NAME}" --target "${UPSTREAM_BRANCH}" || { echo "Failed to create release"; exit 1; } - - -echo "Release ${TAG_NAME} created successfully." diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml deleted file mode 100644 index 775003e..0000000 --- a/.github/workflows/ci.yaml +++ /dev/null @@ -1,71 +0,0 @@ -name: Build and Push container -run-name: Build and Push container -on: - workflow_dispatch: - #schedule: - # - cron: "0 10 * * *" - push: - branches: - - 'main' - - 'master' - tags: - - 'v*' - pull_request: - branches: - - 'main' - - 'master' -env: - IMAGE: git.kvant.cloud/${{github.repository}} -jobs: - build_concierge_backend: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set current time - uses: https://github.com/gerred/actions/current-time@master - id: current_time - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to git.kvant.cloud registry - uses: docker/login-action@v3 - with: - registry: git.kvant.cloud - username: ${{ vars.ORG_PACKAGE_WRITER_USERNAME }} - password: ${{ secrets.ORG_PACKAGE_WRITER_TOKEN }} - - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - # list of Docker images to use as base name for tags - images: | - ${{env.IMAGE}} - # generate Docker tags based on the following events/attributes - tags: | - type=schedule - type=ref,event=branch - type=ref,event=pr - type=semver,pattern={{version}} - - - name: Build and push to gitea registry - uses: docker/build-push-action@v6 - with: - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - context: . - provenance: mode=max - sbom: true - build-args: | - BUILD_DATE=${{ steps.current_time.outputs.time }} - cache-from: | - type=registry,ref=${{ env.IMAGE }}:buildcache - type=registry,ref=${{ env.IMAGE }}:${{ github.ref_name }} - type=registry,ref=${{ env.IMAGE }}:main - cache-to: type=registry,ref=${{ env.IMAGE }}:buildcache,mode=max,image-manifest=true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 0c51bc7..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,64 +0,0 @@ -# -# Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). -# -# This software is the property of WSO2 LLC. and its suppliers, if any. -# Dissemination of any information or reproduction of any material contained -# herein in any form is strictly forbidden, unless permitted by WSO2 expressly. -# You may not alter or remove any copyright or other notice from copies of this content. -# - -name: Release - -on: - workflow_dispatch: - inputs: - version_type: - type: choice - description: Choose the type of version update - options: - - 'major' - - 'minor' - - 'patch' - required: true - -jobs: - update-and-release: - runs-on: ubuntu-latest - env: - GOPROXY: https://proxy.golang.org - if: github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' - steps: - - uses: actions/checkout@v2 - with: - ref: 'main' - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/checkout@v2 - - - name: Set up Go 1.x - uses: actions/setup-go@v3 - with: - go-version: "^1.x" - - - name: Cache Go modules - id: cache-go-modules - uses: actions/cache@v3 - with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-go-modules-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go-modules- - - - name: Install dependencies - run: go mod download - - - name: Build and test - run: make build - working-directory: . - - - name: Update artifact version, package, commit, and create release. - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: bash ./.github/scripts/release.sh $GITHUB_TOKEN ${{ github.workspace }} ${{ github.event.inputs.version_type }} diff --git a/.gitignore b/.gitignore index d200b58..2a2b503 100644 --- a/.gitignore +++ b/.gitignore @@ -24,15 +24,10 @@ hs_err_pid* replay_pid* +# Go module cache files +go.sum + # OS generated files .DS_Store -# builds -build - -# test out files -coverage.out -coverage.html - -# IDE files -.vscode +openmcpauthproxy diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index dc468b1..0000000 --- a/Dockerfile +++ /dev/null @@ -1,48 +0,0 @@ -FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.24@sha256:d9db32125db0c3a680cfb7a1afcaefb89c898a075ec148fdc2f0f646cc2ed509 AS build - -ARG TARGETPLATFORM -ARG BUILDPLATFORM -ARG TARGETOS -ARG TARGETARCH - -WORKDIR /workspace - -RUN apt update -qq && apt install -qq -y git bash curl g++ - -# Download libraries -ADD go.* . -RUN go mod download - -# Build -ADD cmd cmd -ADD internal internal -RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o webhook -ldflags '-w -extldflags "-static"' -o openmcpauthproxy ./cmd/proxy - -#Test -RUN CGO_ENABLED=1 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go test -v -race ./... - - -# Build production container -FROM --platform=${BUILDPLATFORM:-linux/amd64} ubuntu:24.04 - -RUN apt-get update \ - && apt-get install --no-install-recommends -y \ - python3-pip \ - python-is-python3 \ - npm \ - && apt-get autoremove \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -RUN pip install uvenv --break-system-packages - -WORKDIR /app -COPY --from=build /workspace/openmcpauthproxy /app/ - -ADD config.yaml /app - - -ENTRYPOINT ["/app/openmcpauthproxy"] - -ARG IMAGE_SOURCE -LABEL org.opencontainers.image.source=$IMAGE_SOURCE diff --git a/Makefile b/Makefile deleted file mode 100644 index b0d0926..0000000 --- a/Makefile +++ /dev/null @@ -1,88 +0,0 @@ -# Makefile for open-mcp-auth-proxy - -# Variables -PROJECT_ROOT := $(realpath $(dir $(abspath $(lastword $(MAKEFILE_LIST))))) -BINARY_NAME := openmcpauthproxy -GO := go -GOFMT := gofmt -GOVET := go vet -GOTEST := go test -GOLINT := golangci-lint -GOCOV := go tool cover -BUILD_DIR := build - -# Source files -SRC := $(shell find . -name "*.go" -not -path "./vendor/*") -PKGS := $(shell go list ./... | grep -v /vendor/) - -# Set build options -BUILD_OPTS := -v - -# Set test options -TEST_OPTS := -v -race - -.PHONY: all clean test fmt lint vet coverage help - -# Default target -all: lint test build-linux build-linux-arm build-darwin - -build: clean test build-linux build-linux-arm build-darwin - -build-linux: - mkdir -p $(BUILD_DIR)/linux - GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -x -ldflags "-X main.version=$(BUILD_VERSION)" \ - -o $(BUILD_DIR)/linux/openmcpauthproxy $(PROJECT_ROOT)/cmd/proxy - cp config.yaml $(BUILD_DIR)/linux - -build-linux-arm: - mkdir -p $(BUILD_DIR)/linux-arm - GOOS=linux GOARCH=arm CGO_ENABLED=0 go build -x -ldflags "-X main.version=$(BUILD_VERSION)" \ - -o $(BUILD_DIR)/linux-arm/openmcpauthproxy $(PROJECT_ROOT)/cmd/proxy - cp config.yaml $(BUILD_DIR)/linux-arm - -build-darwin: - mkdir -p $(BUILD_DIR)/darwin - GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -x -ldflags "-X main.version=$(BUILD_VERSION)" \ - -o $(BUILD_DIR)/darwin/openmcpauthproxy $(PROJECT_ROOT)/cmd/proxy - cp config.yaml $(BUILD_DIR)/darwin - -# Clean build artifacts -clean: - @echo "Cleaning build artifacts..." - @rm -rf $(BUILD_DIR) - @rm -f coverage.out - -# Run tests -test: - @echo "Running tests..." - $(GOTEST) $(TEST_OPTS) ./... - -# Run tests with coverage report -coverage: - @echo "Running tests with coverage..." - @$(GOTEST) -coverprofile=coverage.out ./... - @$(GOCOV) -func=coverage.out - @$(GOCOV) -html=coverage.out -o coverage.html - @echo "Coverage report generated in coverage.html" - -# Run gofmt -fmt: - @echo "Running gofmt..." - @$(GOFMT) -w -s $(SRC) - -# Run go vet -vet: - @echo "Running go vet..." - @$(GOVET) ./... - -# Show help -help: - @echo "Available targets:" - @echo " all : Run lint, test, and build" - @echo " build : Build the application" - @echo " clean : Clean build artifacts" - @echo " test : Run tests" - @echo " coverage : Run tests with coverage report" - @echo " fmt : Run gofmt" - @echo " vet : Run go vet" - @echo " help : Show this help message" diff --git a/README.md b/README.md index 6be3ece..f197891 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,6 @@ # Open MCP Auth Proxy -A lightweight authorization proxy for Model Context Protocol (MCP) servers that enforces authorization according to the [MCP authorization specification](https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/authorization/) - -[![πŸš€ Release](https://github.com/wso2/open-mcp-auth-proxy/actions/workflows/release.yml/badge.svg)](https://github.com/wso2/open-mcp-auth-proxy/actions/workflows/release.yml) -[![πŸ’¬ Stackoverflow](https://img.shields.io/badge/Ask%20for%20help%20on-Stackoverflow-orange)](https://stackoverflow.com/questions/tagged/wso2is) -[![πŸ’¬ Discord](https://img.shields.io/badge/Join%20us%20on-Discord-%23e01563.svg)](https://discord.gg/wso2) -[![🐦 Twitter](https://img.shields.io/twitter/follow/wso2.svg?style=social&label=Follow)](https://twitter.com/intent/follow?screen_name=wso2) -[![πŸ“ License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/wso2/product-is/blob/master/LICENSE) +A lightweight authorization proxy for Model Context Protocol (MCP) servers that enforces authorization according to the [MCP authorization specification](https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/authorization/). ![Architecture Diagram](https://github.com/user-attachments/assets/41cf6723-c488-4860-8640-8fec45006f92) @@ -25,29 +19,28 @@ Open MCP Auth Proxy sits between MCP clients and your MCP server to: * Go 1.20 or higher * A running MCP server - -> If you don't have an MCP server, you can use the included example: -> -> 1. Navigate to the `resources` directory -> 2. Set up a Python environment: -> -> ```bash -> python3 -m venv .venv -> source .venv/bin/activate -> pip3 install -r requirements.txt -> ``` -> -> 3. Start the example server: -> -> ```bash -> python3 echo_server.py -> ``` - * An MCP client that supports MCP authorization +### Installation + +```bash +git clone https://github.com/wso2/open-mcp-auth-proxy +cd open-mcp-auth-proxy +go get github.com/golang-jwt/jwt/v4 gopkg.in/yaml.v2 +go build -o openmcpauthproxy ./cmd/proxy +``` + ### Basic Usage -1. Download the latest release from [Github releases](https://github.com/wso2/open-mcp-auth-proxy/releases/latest). +1. The repository comes with a default `config.yaml` file that contains the basic configuration: + +```yaml +listen_port: 8080 +base_url: "http://localhost:8000" # Your MCP server URL +paths: + sse: "/sse" + messages: "/messages/" +``` 2. Start the proxy in demo mode (uses pre-configured authentication with Asgardeo sandbox): @@ -55,30 +48,31 @@ Open MCP Auth Proxy sits between MCP clients and your MCP server to: ./openmcpauthproxy --demo ``` -> The repository comes with a default `config.yaml` file that contains the basic configuration: -> -> ```yaml -> listen_port: 8080 -> base_url: "http://localhost:8000" # Your MCP server URL -> paths: -> sse: "/sse" -> messages: "/messages/" -> ``` - 3. Connect using an MCP client like [MCP Inspector](https://github.com/shashimalcse/inspector)(This is a temporary fork with fixes for authentication [issues](https://github.com/modelcontextprotocol/typescript-sdk/issues/257) in the original implementation) -## Connect an Identity Provider +## Identity Provider Integration -### Asgardeo +### Demo Mode -To enable authorization through your Asgardeo organization: +For quick testing, use the `--demo` flag which includes pre-configured authentication and authorization with an Asgardeo sandbox. + +```bash +./openmcpauthproxy --demo +``` + +### Asgardeo Integration + +To enable authorization through your own Asgardeo organization: 1. [Register](https://asgardeo.io/signup) and create an organization in Asgardeo 2. Create an [M2M application](https://wso2.com/asgardeo/docs/guides/applications/register-machine-to-machine-app/) 1. [Authorize this application](https://wso2.com/asgardeo/docs/guides/applications/register-machine-to-machine-app/#authorize-the-api-resources-for-the-app) to invoke "Application Management API" with the `internal_application_mgt_create` scope ![image](https://github.com/user-attachments/assets/0bd57cac-1904-48cc-b7aa-0530224bc41a) - -3. Update `config.yaml` with the following parameters. + 2. Update the existing `config.yaml` with your Asgardeo details: + +#### Configure the Auth Proxy + +Create a configuration file config.yaml with the following parameters: ```yaml base_url: "http://localhost:8000" # URL of your MCP server @@ -90,7 +84,7 @@ asgardeo: client_secret: "" # Client secret of the M2M app ``` -4. Start the proxy with Asgardeo integration: +3. Start the proxy with Asgardeo integration: ```bash ./openmcpauthproxy --asgardeo @@ -98,8 +92,26 @@ asgardeo: ### Other OAuth Providers -- [Auth0](docs/integrations/Auth0.md) -- [Keycloak](docs/integrations/keycloak.md) +- [Auth0 Integration Guide](docs/Auth0.md) + +## Testing with an Example MCP Server + +If you don't have an MCP server, you can use the included example: + +1. Navigate to the `resources` directory +2. Set up a Python environment: + +```bash +python3 -m venv .venv +source .venv/bin/activate +pip3 install -r requirements.txt +``` + +3. Start the example server: + +```bash +python3 echo_server.py +``` # Advanced Configuration @@ -214,13 +226,4 @@ asgardeo: org_name: "" client_id: "" client_secret: "" -``` - -### Build from source - -```bash -git clone https://github.com/wso2/open-mcp-auth-proxy -cd open-mcp-auth-proxy -go get github.com/golang-jwt/jwt/v4 gopkg.in/yaml.v2 -go build -o openmcpauthproxy ./cmd/proxy -``` +``` \ No newline at end of file diff --git a/cmd/proxy/main.go b/cmd/proxy/main.go index c43dd7d..6424f18 100644 --- a/cmd/proxy/main.go +++ b/cmd/proxy/main.go @@ -12,7 +12,7 @@ import ( "github.com/wso2/open-mcp-auth-proxy/internal/authz" "github.com/wso2/open-mcp-auth-proxy/internal/config" "github.com/wso2/open-mcp-auth-proxy/internal/constants" - logger "github.com/wso2/open-mcp-auth-proxy/internal/logging" + "github.com/wso2/open-mcp-auth-proxy/internal/logging" "github.com/wso2/open-mcp-auth-proxy/internal/proxy" "github.com/wso2/open-mcp-auth-proxy/internal/subprocess" "github.com/wso2/open-mcp-auth-proxy/internal/util" @@ -58,7 +58,7 @@ func main() { logger.Warn("%v", err) logger.Warn("Subprocess may fail to start due to missing dependencies") } - + procManager = subprocess.NewManager() if err := procManager.Start(cfg); err != nil { logger.Warn("Failed to start subprocess: %v", err) @@ -95,7 +95,7 @@ func main() { // 5. Build the main router mux := proxy.NewRouter(cfg, provider) - listen_address := fmt.Sprintf("0.0.0.0:%d", cfg.ListenPort) + listen_address := fmt.Sprintf(":%d", cfg.ListenPort) // 6. Start the server srv := &http.Server{ diff --git a/config.yaml b/config.yaml index ef70fbb..5621195 100644 --- a/config.yaml +++ b/config.yaml @@ -6,23 +6,29 @@ base_url: "http://localhost:8000" # Base URL for the MCP server port: 8000 # Port for the MCP server timeout_seconds: 10 +# Path configuration +paths: + sse: "/sse" # SSE endpoint path + messages: "/messages/" # Messages endpoint path # Transport mode configuration -transport_mode: "stdio" # Options: "sse" or "stdio" +transport_mode: "sse" # Options: "sse" or "stdio" # stdio-specific configuration (used only when transport_mode is "stdio") stdio: enabled: true - user_command: uvx mcp-server-time --local-timezone=Europe/Zurich - #user_command: "npx -y @modelcontextprotocol/server-github" + user_command: "npx -y @modelcontextprotocol/server-github" work_dir: "" # Working directory (optional) # env: # Environment variables (optional) # - "NODE_ENV=development" -# CORS settings +# Path mapping (optional) +path_mapping: + +# CORS configuration cors: allowed_origins: - - "http://localhost:6274" # Origin of your frontend/client app + - "http://localhost:5173" allowed_methods: - "GET" - "POST" @@ -34,32 +40,8 @@ cors: - "mcp-protocol-version" allow_credentials: true -# Keycloak endpoint path mappings -path_mapping: - sse: "/sse" # SSE endpoint path - messages: "/messages/" # Messages endpoint path - /token: /realms/master/protocol/openid-connect/token - /register: /realms/master/clients-registrations/openid-connect - -# Keycloak configuration block -default: - base_url: "https://iam.phoenix-systems.ch" - jwks_url: "https://iam.phoenix-systems.ch/realms/kvant/protocol/openid-connect/certs" - path: - /.well-known/oauth-authorization-server: - response: - issuer: "https://iam.phoenix-systems.ch/realms/kvant" - jwks_uri: "https://iam.phoenix-systems.ch/realms/kvant/protocol/openid-connect/certs" - authorization_endpoint: "https://iam.phoenix-systems.ch/realms/kvant/protocol/openid-connect/auth" - response_types_supported: - - "code" - grant_types_supported: - - "authorization_code" - - "refresh_token" - code_challenge_methods_supported: - - "S256" - - "plain" - /token: - addBodyParams: - - name: "audience" - value: "mcp_proxy" \ No newline at end of file +# Demo configuration for Asgardeo +demo: + org_name: "openmcpauthdemo" + client_id: "N0U9e_NNGr9mP_0fPnPfPI0a6twa" + client_secret: "qFHfiBp5gNGAO9zV4YPnDofBzzfInatfUbHyPZvM0jka" diff --git a/go.mod b/go.mod index 0bceb4f..2d26216 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/wso2/open-mcp-auth-proxy -go 1.21 +go 1.22.3 require ( github.com/golang-jwt/jwt/v4 v4.5.2 diff --git a/go.sum b/go.sum deleted file mode 100644 index 9d27ad1..0000000 --- a/go.sum +++ /dev/null @@ -1,6 +0,0 @@ -github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= -github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/internal/authz/default_test.go b/internal/authz/default_test.go deleted file mode 100644 index f40030f..0000000 --- a/internal/authz/default_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package authz - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - "github.com/wso2/open-mcp-auth-proxy/internal/config" -) - -func TestNewDefaultProvider(t *testing.T) { - cfg := &config.Config{} - provider := NewDefaultProvider(cfg) - - if provider == nil { - t.Fatal("Expected non-nil provider") - } - - // Ensure it implements the Provider interface - var _ Provider = provider -} - -func TestDefaultProviderWellKnownHandler(t *testing.T) { - // Create a config with a custom well-known response - cfg := &config.Config{ - Default: config.DefaultConfig{ - Path: map[string]config.PathConfig{ - "/.well-known/oauth-authorization-server": { - Response: &config.ResponseConfig{ - Issuer: "https://test-issuer.com", - JwksURI: "https://test-issuer.com/jwks", - ResponseTypesSupported: []string{"code"}, - GrantTypesSupported: []string{"authorization_code"}, - CodeChallengeMethodsSupported: []string{"S256"}, - }, - }, - }, - }, - } - - provider := NewDefaultProvider(cfg) - handler := provider.WellKnownHandler() - - // Create a test request - req := httptest.NewRequest("GET", "/.well-known/oauth-authorization-server", nil) - req.Host = "test-host.com" - req.Header.Set("X-Forwarded-Proto", "https") - - // Create a response recorder - w := httptest.NewRecorder() - - // Call the handler - handler(w, req) - - // Check response status - if w.Code != http.StatusOK { - t.Errorf("Expected status OK, got %v", w.Code) - } - - // Verify content type - contentType := w.Header().Get("Content-Type") - if contentType != "application/json" { - t.Errorf("Expected Content-Type: application/json, got %s", contentType) - } - - // Decode and check the response body - var response map[string]interface{} - if err := json.NewDecoder(w.Body).Decode(&response); err != nil { - t.Fatalf("Failed to decode response JSON: %v", err) - } - - // Check expected values - if response["issuer"] != "https://test-issuer.com" { - t.Errorf("Expected issuer=https://test-issuer.com, got %v", response["issuer"]) - } - if response["jwks_uri"] != "https://test-issuer.com/jwks" { - t.Errorf("Expected jwks_uri=https://test-issuer.com/jwks, got %v", response["jwks_uri"]) - } - if response["authorization_endpoint"] != "https://test-host.com/authorize" { - t.Errorf("Expected authorization_endpoint=https://test-host.com/authorize, got %v", response["authorization_endpoint"]) - } -} - -func TestDefaultProviderHandleOPTIONS(t *testing.T) { - provider := NewDefaultProvider(&config.Config{}) - handler := provider.WellKnownHandler() - - // Create OPTIONS request - req := httptest.NewRequest("OPTIONS", "/.well-known/oauth-authorization-server", nil) - w := httptest.NewRecorder() - - // Call the handler - handler(w, req) - - // Check response - if w.Code != http.StatusNoContent { - t.Errorf("Expected status NoContent for OPTIONS request, got %v", w.Code) - } - - // Check CORS headers - if w.Header().Get("Access-Control-Allow-Origin") != "*" { - t.Errorf("Expected Access-Control-Allow-Origin: *, got %s", w.Header().Get("Access-Control-Allow-Origin")) - } - if w.Header().Get("Access-Control-Allow-Methods") != "GET, OPTIONS" { - t.Errorf("Expected Access-Control-Allow-Methods: GET, OPTIONS, got %s", w.Header().Get("Access-Control-Allow-Methods")) - } -} - -func TestDefaultProviderInvalidMethod(t *testing.T) { - provider := NewDefaultProvider(&config.Config{}) - handler := provider.WellKnownHandler() - - // Create POST request (which should be rejected) - req := httptest.NewRequest("POST", "/.well-known/oauth-authorization-server", nil) - w := httptest.NewRecorder() - - // Call the handler - handler(w, req) - - // Check response - if w.Code != http.StatusMethodNotAllowed { - t.Errorf("Expected status MethodNotAllowed for POST request, got %v", w.Code) - } -} diff --git a/internal/config/config_test.go b/internal/config/config_test.go deleted file mode 100644 index 20c0893..0000000 --- a/internal/config/config_test.go +++ /dev/null @@ -1,196 +0,0 @@ -package config - -import ( - "os" - "path/filepath" - "testing" -) - -func TestLoadConfig(t *testing.T) { - // Create a temporary config file - tempDir := t.TempDir() - configPath := filepath.Join(tempDir, "test_config.yaml") - - // Basic valid config - validConfig := ` -listen_port: 8080 -base_url: "http://localhost:8000" -transport_mode: "sse" -paths: - sse: "/sse" - messages: "/messages" -cors: - allowed_origins: - - "http://localhost:5173" - allowed_methods: - - "GET" - - "POST" - allowed_headers: - - "Authorization" - - "Content-Type" - allow_credentials: true -` - err := os.WriteFile(configPath, []byte(validConfig), 0644) - if err != nil { - t.Fatalf("Failed to create test config file: %v", err) - } - - // Test loading the valid config - cfg, err := LoadConfig(configPath) - if err != nil { - t.Fatalf("Failed to load valid config: %v", err) - } - - // Verify expected values from the config - if cfg.ListenPort != 8080 { - t.Errorf("Expected ListenPort=8080, got %d", cfg.ListenPort) - } - if cfg.BaseURL != "http://localhost:8000" { - t.Errorf("Expected BaseURL=http://localhost:8000, got %s", cfg.BaseURL) - } - if cfg.TransportMode != SSETransport { - t.Errorf("Expected TransportMode=sse, got %s", cfg.TransportMode) - } - if cfg.Paths.SSE != "/sse" { - t.Errorf("Expected Paths.SSE=/sse, got %s", cfg.Paths.SSE) - } - if cfg.Paths.Messages != "/messages" { - t.Errorf("Expected Paths.Messages=/messages, got %s", cfg.Paths.Messages) - } - - // Test default values - if cfg.TimeoutSeconds != 15 { - t.Errorf("Expected default TimeoutSeconds=15, got %d", cfg.TimeoutSeconds) - } - if cfg.Port != 8000 { - t.Errorf("Expected default Port=8000, got %d", cfg.Port) - } -} - -func TestValidate(t *testing.T) { - tests := []struct { - name string - config Config - expectError bool - }{ - { - name: "Valid SSE config", - config: Config{ - TransportMode: SSETransport, - Paths: PathsConfig{ - SSE: "/sse", - Messages: "/messages", - }, - BaseURL: "http://localhost:8000", - }, - expectError: false, - }, - { - name: "Valid stdio config", - config: Config{ - TransportMode: StdioTransport, - Stdio: StdioConfig{ - Enabled: true, - UserCommand: "some-command", - }, - }, - expectError: false, - }, - { - name: "Invalid stdio config - not enabled", - config: Config{ - TransportMode: StdioTransport, - Stdio: StdioConfig{ - Enabled: false, - UserCommand: "some-command", - }, - }, - expectError: true, - }, - { - name: "Invalid stdio config - no command", - config: Config{ - TransportMode: StdioTransport, - Stdio: StdioConfig{ - Enabled: true, - UserCommand: "", - }, - }, - expectError: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - err := tc.config.Validate() - if tc.expectError && err == nil { - t.Errorf("Expected validation error but got none") - } - if !tc.expectError && err != nil { - t.Errorf("Expected no validation error but got: %v", err) - } - }) - } -} - -func TestGetMCPPaths(t *testing.T) { - cfg := Config{ - Paths: PathsConfig{ - SSE: "/custom-sse", - Messages: "/custom-messages", - }, - } - - paths := cfg.GetMCPPaths() - if len(paths) != 2 { - t.Errorf("Expected 2 MCP paths, got %d", len(paths)) - } - if paths[0] != "/custom-sse" { - t.Errorf("Expected first path=/custom-sse, got %s", paths[0]) - } - if paths[1] != "/custom-messages" { - t.Errorf("Expected second path=/custom-messages, got %s", paths[1]) - } -} - -func TestBuildExecCommand(t *testing.T) { - tests := []struct { - name string - config Config - expectedResult string - }{ - { - name: "Valid command", - config: Config{ - Stdio: StdioConfig{ - UserCommand: "test-command", - }, - Port: 8080, - BaseURL: "http://example.com", - Paths: PathsConfig{ - SSE: "/sse-path", - Messages: "/msgs", - }, - }, - expectedResult: `npx -y supergateway --stdio "test-command" --port 8080 --baseUrl http://example.com --ssePath /sse-path --messagePath /msgs`, - }, - { - name: "Empty command", - config: Config{ - Stdio: StdioConfig{ - UserCommand: "", - }, - }, - expectedResult: "", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - result := tc.config.BuildExecCommand() - if result != tc.expectedResult { - t.Errorf("Expected command=%s, got %s", tc.expectedResult, result) - } - }) - } -} diff --git a/internal/proxy/modifier_test.go b/internal/proxy/modifier_test.go deleted file mode 100644 index 3d2fd44..0000000 --- a/internal/proxy/modifier_test.go +++ /dev/null @@ -1,147 +0,0 @@ -package proxy - -import ( - "net/http" - "net/url" - "strings" - "testing" - - "github.com/wso2/open-mcp-auth-proxy/internal/config" -) - -func TestAuthorizationModifier(t *testing.T) { - cfg := &config.Config{ - Default: config.DefaultConfig{ - Path: map[string]config.PathConfig{ - "/authorize": { - AddQueryParams: []config.ParamConfig{ - {Name: "client_id", Value: "test-client-id"}, - {Name: "scope", Value: "openid"}, - }, - }, - }, - }, - } - - modifier := &AuthorizationModifier{Config: cfg} - - // Create a test request - req, err := http.NewRequest("GET", "/authorize?response_type=code", nil) - if err != nil { - t.Fatalf("Failed to create request: %v", err) - } - - // Modify the request - modifiedReq, err := modifier.ModifyRequest(req) - if err != nil { - t.Fatalf("ModifyRequest failed: %v", err) - } - - // Check that the query parameters were added - query := modifiedReq.URL.Query() - if query.Get("client_id") != "test-client-id" { - t.Errorf("Expected client_id=test-client-id, got %s", query.Get("client_id")) - } - if query.Get("scope") != "openid" { - t.Errorf("Expected scope=openid, got %s", query.Get("scope")) - } - if query.Get("response_type") != "code" { - t.Errorf("Expected response_type=code, got %s", query.Get("response_type")) - } -} - -func TestTokenModifier(t *testing.T) { - cfg := &config.Config{ - Default: config.DefaultConfig{ - Path: map[string]config.PathConfig{ - "/token": { - AddBodyParams: []config.ParamConfig{ - {Name: "audience", Value: "test-audience"}, - }, - }, - }, - }, - } - - modifier := &TokenModifier{Config: cfg} - - // Create a test request with form data - form := url.Values{} - - req, err := http.NewRequest("POST", "/token", strings.NewReader(form.Encode())) - if err != nil { - t.Fatalf("Failed to create request: %v", err) - } - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - - // Modify the request - modifiedReq, err := modifier.ModifyRequest(req) - if err != nil { - t.Fatalf("ModifyRequest failed: %v", err) - } - - body := make([]byte, 1024) - n, err := modifiedReq.Body.Read(body) - if err != nil && err.Error() != "EOF" { - t.Fatalf("Failed to read body: %v", err) - } - bodyStr := string(body[:n]) - - // Parse the form data from the modified request - if err := modifiedReq.ParseForm(); err != nil { - t.Fatalf("Failed to parse form data: %v", err) - } - - // Check that the body parameters were added - if !strings.Contains(bodyStr, "audience") { - t.Errorf("Expected body to contain audience, got %s", bodyStr) - } -} - -func TestRegisterModifier(t *testing.T) { - cfg := &config.Config{ - Default: config.DefaultConfig{ - Path: map[string]config.PathConfig{ - "/register": { - AddBodyParams: []config.ParamConfig{ - {Name: "client_name", Value: "test-client"}, - }, - }, - }, - }, - } - - modifier := &RegisterModifier{Config: cfg} - - // Create a test request with JSON data - jsonBody := `{"redirect_uris":["https://example.com/callback"]}` - req, err := http.NewRequest("POST", "/register", strings.NewReader(jsonBody)) - if err != nil { - t.Fatalf("Failed to create request: %v", err) - } - req.Header.Set("Content-Type", "application/json") - - // Modify the request - modifiedReq, err := modifier.ModifyRequest(req) - if err != nil { - t.Fatalf("ModifyRequest failed: %v", err) - } - - // Read the body and check that it still contains the original data - // This test would need to be enhanced with a proper JSON parsing to verify - // the added parameters - body := make([]byte, 1024) - n, err := modifiedReq.Body.Read(body) - if err != nil && err.Error() != "EOF" { - t.Fatalf("Failed to read body: %v", err) - } - bodyStr := string(body[:n]) - - // Simple check to see if the modified body contains the expected fields - if !strings.Contains(bodyStr, "client_name") { - t.Errorf("Expected body to contain client_name, got %s", bodyStr) - } - if !strings.Contains(bodyStr, "redirect_uris") { - t.Errorf("Expected body to contain redirect_uris, got %s", bodyStr) - } -} diff --git a/internal/util/jwks_test.go b/internal/util/jwks_test.go deleted file mode 100644 index 3b00c68..0000000 --- a/internal/util/jwks_test.go +++ /dev/null @@ -1,143 +0,0 @@ -package util - -import ( - "crypto/rand" - "crypto/rsa" - "encoding/base64" - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/golang-jwt/jwt/v4" -) - -func TestValidateJWT(t *testing.T) { - // Initialize the test JWKS data - initTestJWKS(t) - - // Test cases - tests := []struct { - name string - authHeader string - expectError bool - }{ - { - name: "Valid JWT token", - authHeader: "Bearer " + createValidJWT(t), - expectError: false, - }, - { - name: "No auth header", - authHeader: "", - expectError: true, - }, - { - name: "Invalid auth header format", - authHeader: "InvalidFormat", - expectError: true, - }, - { - name: "Invalid JWT token", - authHeader: "Bearer invalid.jwt.token", - expectError: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - err := ValidateJWT(tc.authHeader) - if tc.expectError && err == nil { - t.Errorf("Expected error but got none") - } - if !tc.expectError && err != nil { - t.Errorf("Expected no error but got: %v", err) - } - }) - } -} - -func TestFetchJWKS(t *testing.T) { - // Create a mock JWKS server - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Generate a test RSA key - privateKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - t.Fatalf("Failed to generate RSA key: %v", err) - } - - // Create JWKS response - jwks := map[string]interface{}{ - "keys": []map[string]interface{}{ - { - "kty": "RSA", - "kid": "test-key-id", - "n": base64.RawURLEncoding.EncodeToString(privateKey.N.Bytes()), - "e": base64.RawURLEncoding.EncodeToString([]byte{1, 0, 1}), // Default exponent 65537 - }, - }, - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(jwks) - })) - defer server.Close() - - // Test fetching JWKS - err := FetchJWKS(server.URL) - if err != nil { - t.Fatalf("FetchJWKS failed: %v", err) - } - - // Check that keys were stored - if len(publicKeys) == 0 { - t.Errorf("Expected publicKeys to be populated") - } -} - -// Helper function to initialize test JWKS data -func initTestJWKS(t *testing.T) { - // Create a test RSA key pair - privateKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - t.Fatalf("Failed to generate RSA key: %v", err) - } - - // Initialize the publicKeys map - publicKeys = map[string]*rsa.PublicKey{ - "test-key-id": &privateKey.PublicKey, - } -} - -// Helper function to create a valid JWT token for testing -func createValidJWT(t *testing.T) string { - // Create a test RSA key pair - privateKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - t.Fatalf("Failed to generate RSA key: %v", err) - } - - // Ensure the test key is in the publicKeys map - if publicKeys == nil { - publicKeys = map[string]*rsa.PublicKey{} - } - publicKeys["test-key-id"] = &privateKey.PublicKey - - // Create token - token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{ - "sub": "1234567890", - "name": "Test User", - "iat": time.Now().Unix(), - "exp": time.Now().Add(time.Hour).Unix(), - }) - token.Header["kid"] = "test-key-id" - - // Sign the token - tokenString, err := token.SignedString(privateKey) - if err != nil { - t.Fatalf("Failed to sign token: %v", err) - } - - return tokenString -} diff --git a/pull_request_template.md b/pull_request_template.md index c401a06..9b32185 100644 --- a/pull_request_template.md +++ b/pull_request_template.md @@ -1,11 +1,52 @@ ## Purpose - +> Describe the problems, issues, or needs driving this feature/fix and include links to related issues in the following format: Resolves issue1, issue2, etc. -## Related Issues - +## Goals +> Describe the solutions that this feature/fix will introduce to resolve the problems described above + +## Approach +> Describe how you are implementing the solutions. Include an animated GIF or screenshot if the change affects the UI (email documentation@wso2.com to review all UI text). Include a link to a Markdown file or Google doc if the feature write-up is too long to paste here. + +## User stories +> Summary of user stories addressed by this change> + +## Release note +> Brief description of the new feature or bug fix as it will appear in the release notes + +## Documentation +> Link(s) to product documentation that addresses the changes of this PR. If no doc impact, enter β€œN/A” plus brief explanation of why there’s no doc impact + +## Training +> Link to the PR for changes to the training content in https://github.com/wso2/WSO2-Training, if applicable + +## Certification +> Type β€œSent” when you have provided new/updated certification questions, plus four answers for each question (correct answer highlighted in bold), based on this change. Certification questions/answers should be sent to certification@wso2.com and NOT pasted in this PR. If there is no impact on certification exams, type β€œN/A” and explain why. + +## Marketing +> Link to drafts of marketing content that will describe and promote this feature, including product page changes, technical articles, blog posts, videos, etc., if applicable + +## Automation tests + - Unit tests + > Code coverage information + - Integration tests + > Details about the test cases and coverage + +## Security checks + - Followed secure coding standards in http://wso2.com/technical-reports/wso2-secure-engineering-guidelines? yes/no + - Ran FindSecurityBugs plugin and verified report? yes/no + - Confirmed that this PR doesn't commit any keys, passwords, tokens, usernames, or other secrets? yes/no + +## Samples +> Provide high-level details about the samples related to this feature ## Related PRs - +> List any other related PRs ## Migrations (if applicable) - +> Describe migration steps and platforms on which migration has been tested + +## Test environment +> List all JDK versions, operating systems, databases, and browser/versions on which this feature/fix was tested + +## Learning +> Describe the research phase and any blog posts, patterns, libraries, or add-ons you used to solve the problem. \ No newline at end of file