Compare commits
No commits in common. "main" and "keycloak" have entirely different histories.
16 changed files with 127 additions and 1118 deletions
124
.github/scripts/release.sh
vendored
124
.github/scripts/release.sh
vendored
|
@ -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."
|
71
.github/workflows/ci.yaml
vendored
71
.github/workflows/ci.yaml
vendored
|
@ -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
|
64
.github/workflows/release.yml
vendored
64
.github/workflows/release.yml
vendored
|
@ -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 }}
|
13
.gitignore
vendored
13
.gitignore
vendored
|
@ -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
|
||||
|
|
48
Dockerfile
48
Dockerfile
|
@ -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
|
88
Makefile
88
Makefile
|
@ -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"
|
107
README.md
107
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/)
|
||||
|
||||
<a href="">[](https://github.com/wso2/open-mcp-auth-proxy/actions/workflows/release.yml)</a>
|
||||
<a href="">[](https://stackoverflow.com/questions/tagged/wso2is)</a>
|
||||
<a href="">[](https://discord.gg/wso2)</a>
|
||||
<a href="">[](https://twitter.com/intent/follow?screen_name=wso2)</a>
|
||||
<a href="">[](https://github.com/wso2/product-is/blob/master/LICENSE)</a>
|
||||
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/).
|
||||
|
||||

|
||||
|
||||
|
@ -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
|
||||

|
||||
2. Update the existing `config.yaml` with your Asgardeo details:
|
||||
|
||||
3. Update `config.yaml` with the following parameters.
|
||||
#### 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>" # 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
|
||||
|
||||
|
@ -215,12 +227,3 @@ asgardeo:
|
|||
client_id: "<client_id>"
|
||||
client_secret: "<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
|
||||
```
|
||||
|
|
|
@ -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"
|
||||
|
@ -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{
|
||||
|
|
50
config.yaml
50
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"
|
||||
# Demo configuration for Asgardeo
|
||||
demo:
|
||||
org_name: "openmcpauthdemo"
|
||||
client_id: "N0U9e_NNGr9mP_0fPnPfPI0a6twa"
|
||||
client_secret: "qFHfiBp5gNGAO9zV4YPnDofBzzfInatfUbHyPZvM0jka"
|
||||
|
|
2
go.mod
2
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
|
||||
|
|
6
go.sum
6
go.sum
|
@ -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=
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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. -->
|
||||
> 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
|
||||
<!-- List any 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 -->
|
||||
> List any other related PRs
|
||||
|
||||
## Migrations (if applicable)
|
||||
<!-- Describe migration steps and platforms on which migration has been tested -->
|
||||
> 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.
|
Loading…
Add table
Add a link
Reference in a new issue