Skip to content

Commit f8edabe

Browse files
authored
feat: add OIDC provider support (#40)
* feat: add OIDC provider support - Add OIDC provider implementation with configurable endpoints - Support for custom user ID field mapping using JSON pointer - Add comprehensive CLI flags and environment variables for OIDC configuration - Update README with OIDC setup instructions and configuration options - Include new GitHub Actions workflow for automated checks * ci: add codecov configuration
1 parent 54f94c6 commit f8edabe

9 files changed

Lines changed: 252 additions & 0 deletions

File tree

.github/workflows/check.yaml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Check
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- main
10+
11+
jobs:
12+
check:
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- name: Checkout repository
17+
uses: actions/checkout@v4
18+
19+
- name: Set up Go
20+
uses: actions/setup-go@v4
21+
with:
22+
go-version: "1.23"
23+
24+
- run: go test ./...
25+
- run: go vet ./...
26+
- uses: dominikh/[email protected]
27+
with:
28+
install-go: false

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,13 @@ For a simpler approach to publish local MCP servers over OAuth, consider [MCP Wa
116116
| `GITHUB_CLIENT_ID` | No | GitHub OAuth client ID | - |
117117
| `GITHUB_CLIENT_SECRET` | No | GitHub OAuth client secret | - |
118118
| `GITHUB_ALLOWED_USERS` | No | Comma-separated list of allowed GitHub usernames | - |
119+
| `OIDC_CONFIGURATION_URL` | No | OIDC configuration URL | - |
120+
| `OIDC_CLIENT_ID` | No | OIDC client ID | - |
121+
| `OIDC_CLIENT_SECRET` | No | OIDC client secret | - |
122+
| `OIDC_SCOPES` | No | Comma-separated list of OIDC scopes | `openid,profile,email` |
123+
| `OIDC_USER_ID_FIELD` | No | JSON pointer to user ID field in userinfo endpoint response | `/email` |
124+
| `OIDC_PROVIDER_NAME` | No | Display name for OIDC provider | `OIDC` |
125+
| `OIDC_ALLOWED_USERS` | No | Comma-separated list of allowed OIDC users | - |
119126
| `PASSWORD` | No | Plain text password (will be hashed with bcrypt) | - |
120127
| `PASSWORD_HASH` | No | Bcrypt hash of password for authentication | - |
121128
| `PROXY_BEARER_TOKEN` | No | Bearer token to add to Authorization header when proxying requests | - |
@@ -135,6 +142,13 @@ For a simpler approach to publish local MCP servers over OAuth, consider [MCP Wa
135142
1. Go to the [Register new GitHub App](https://github.com/settings/apps/new)
136143
2. Set Authorization callback URL: `{EXTERNAL_URL}/.auth/github/callback`
137144

145+
#### OIDC Provider Setup
146+
1. Configure your OIDC provider (e.g., Keycloak, Auth0, Azure AD, etc.)
147+
2. Create a new client application
148+
3. Set redirect URI: `{EXTERNAL_URL}/.auth/oidc/callback`
149+
4. Note the configuration URL (usually issuer URL + /.well-known/openid-configuration), client ID, and client secret
150+
5. Configure the userinfo endpoint to return user identification field (default: email)
151+
138152
## 🚀 Usage
139153

140154
### Method 1: Download Binary
@@ -152,6 +166,10 @@ Download the latest binary from [releases](https://github.com/sigbit/mcp-auth-pr
152166
--github-client-id "your-github-client-id" \
153167
--github-client-secret "your-github-client-secret" \
154168
--github-allowed-users "username1,username2" \
169+
--oidc-configuration-url "https://your-oidc-provider.com/.well-known/openid-configuration" \
170+
--oidc-client-id "your-oidc-client-id" \
171+
--oidc-client-secret "your-oidc-client-secret" \
172+
--oidc-allowed-users "[email protected],[email protected]" \
155173
http://localhost:8080
156174
```
157175

@@ -168,6 +186,10 @@ docker run --rm --net=host \
168186
-e GITHUB_CLIENT_ID="your-github-client-id" \
169187
-e GITHUB_CLIENT_SECRET="your-github-client-secret" \
170188
-e GITHUB_ALLOWED_USERS="username1,username2" \
189+
-e OIDC_CONFIGURATION_URL="https://your-oidc-provider.com/.well-known/openid-configuration" \
190+
-e OIDC_CLIENT_ID="your-oidc-client-id" \
191+
-e OIDC_CLIENT_SECRET="your-oidc-client-secret" \
192+
-e OIDC_ALLOWED_USERS="[email protected],[email protected]" \
171193
-v ./data:/data \
172194
ghcr.io/sigbit/mcp-auth-proxy:latest \
173195
http://localhost:8080

codecov.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
coverage:
2+
status:
3+
project:
4+
default:
5+
informational: true
6+
patch:
7+
default:
8+
informational: true

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require (
99
github.com/gin-gonic/gin v1.10.1
1010
github.com/golang-jwt/jwt/v5 v5.3.0
1111
github.com/mark3labs/mcp-go v0.37.0
12+
github.com/mattn/go-jsonpointer v0.0.1
1213
github.com/ory/fosite v0.49.0
1314
github.com/spf13/cobra v1.8.1
1415
github.com/stretchr/testify v1.10.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,8 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
359359
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
360360
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
361361
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
362+
github.com/mattn/go-jsonpointer v0.0.1 h1:j5m5P9BdP4B/zn6J7oH3KIQSOa2OHmcNAKEozUW7wuE=
363+
github.com/mattn/go-jsonpointer v0.0.1/go.mod h1:1s8vx7JSjlgVRF+LW16MPpWSRZAxyrc1/FYzOonxeao=
362364
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
363365
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
364366
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=

main.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ func main() {
4040
var githubClientID string
4141
var githubClientSecret string
4242
var githubAllowedUsers string
43+
var oidcConfigurationURL string
44+
var oidcClientID string
45+
var oidcClientSecret string
46+
var oidcScopes string
47+
var oidcUserIDField string
48+
var oidcProviderName string
49+
var oidcAllowedUsers string
4350
var password string
4451
var passwordHash string
4552
var proxyBearerToken string
@@ -64,6 +71,24 @@ func main() {
6471
}
6572
}
6673

74+
var oidcAllowedUsersList []string
75+
if oidcAllowedUsers != "" {
76+
oidcAllowedUsersList = strings.Split(oidcAllowedUsers, ",")
77+
for i := range oidcAllowedUsersList {
78+
oidcAllowedUsersList[i] = strings.TrimSpace(oidcAllowedUsersList[i])
79+
}
80+
}
81+
82+
var oidcScopesList []string
83+
if oidcScopes != "" {
84+
oidcScopesList = strings.Split(oidcScopes, ",")
85+
for i := range oidcScopesList {
86+
oidcScopesList[i] = strings.TrimSpace(oidcScopesList[i])
87+
}
88+
} else {
89+
oidcScopesList = []string{"openid", "profile", "email"}
90+
}
91+
6792
// Parse proxy headers into slice
6893
var proxyHeadersList []string
6994
if proxyHeaders != "" {
@@ -88,6 +113,13 @@ func main() {
88113
githubClientID,
89114
githubClientSecret,
90115
githubAllowedUsersList,
116+
oidcConfigurationURL,
117+
oidcClientID,
118+
oidcClientSecret,
119+
oidcScopesList,
120+
oidcUserIDField,
121+
oidcProviderName,
122+
oidcAllowedUsersList,
91123
password,
92124
passwordHash,
93125
proxyHeadersList,
@@ -118,6 +150,15 @@ func main() {
118150
rootCmd.Flags().StringVar(&githubClientSecret, "github-client-secret", getEnvWithDefault("GITHUB_CLIENT_SECRET", ""), "GitHub OAuth client secret")
119151
rootCmd.Flags().StringVar(&githubAllowedUsers, "github-allowed-users", getEnvWithDefault("GITHUB_ALLOWED_USERS", ""), "Comma-separated list of allowed GitHub users (usernames)")
120152

153+
// OIDC configuration
154+
rootCmd.Flags().StringVar(&oidcConfigurationURL, "oidc-configuration-url", getEnvWithDefault("OIDC_CONFIGURATION_URL", ""), "OIDC configuration URL")
155+
rootCmd.Flags().StringVar(&oidcClientID, "oidc-client-id", getEnvWithDefault("OIDC_CLIENT_ID", ""), "OIDC client ID")
156+
rootCmd.Flags().StringVar(&oidcClientSecret, "oidc-client-secret", getEnvWithDefault("OIDC_CLIENT_SECRET", ""), "OIDC client secret")
157+
rootCmd.Flags().StringVar(&oidcScopes, "oidc-scopes", getEnvWithDefault("OIDC_SCOPES", "openid,profile,email"), "Comma-separated list of OIDC scopes")
158+
rootCmd.Flags().StringVar(&oidcUserIDField, "oidc-user-id-field", getEnvWithDefault("OIDC_USER_ID_FIELD", "/email"), "JSON pointer to user ID field in userinfo endpoint response")
159+
rootCmd.Flags().StringVar(&oidcProviderName, "oidc-provider-name", getEnvWithDefault("OIDC_PROVIDER_NAME", "OIDC"), "Display name for OIDC provider")
160+
rootCmd.Flags().StringVar(&oidcAllowedUsers, "oidc-allowed-users", getEnvWithDefault("OIDC_ALLOWED_USERS", ""), "Comma-separated list of allowed OIDC users")
161+
121162
// Password authentication
122163
rootCmd.Flags().StringVar(&password, "password", getEnvWithDefault("PASSWORD", ""), "Plain text password for authentication (will be hashed with bcrypt)")
123164
rootCmd.Flags().StringVar(&passwordHash, "password-hash", getEnvWithDefault("PASSWORD_HASH", ""), "Bcrypt hash of password for authentication")

pkg/auth/auth.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ const (
5353
GoogleCallbackEndpoint = "/.auth/google/callback"
5454
GitHubAuthEndpoint = "/.auth/github"
5555
GitHubCallbackEndpoint = "/.auth/github/callback"
56+
OIDCAuthEndpoint = "/.auth/oidc"
57+
OIDCCallbackEndpoint = "/.auth/oidc/callback"
5658

5759
PasswordProvider = "password"
5860
PasswordUserID = "password_user"

pkg/auth/oidc.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package auth
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"net/http"
8+
"net/url"
9+
10+
"github.com/gin-gonic/gin"
11+
"github.com/mattn/go-jsonpointer"
12+
"golang.org/x/oauth2"
13+
)
14+
15+
type oidcProvider struct {
16+
oauth2 oauth2.Config
17+
providerName string
18+
userInfoURL string
19+
userIDField string
20+
allowedUsers []string
21+
}
22+
23+
func NewOIDCProvider(
24+
configurationURL string, scopes []string, userIDField string,
25+
providerName, externalURL, clientID, clientSecret string, allowedUsers []string,
26+
) (Provider, error) {
27+
resp, err := http.Get(configurationURL)
28+
if err != nil {
29+
return nil, err
30+
}
31+
defer resp.Body.Close()
32+
var cfg struct {
33+
AuthEndpoint string `json:"authorization_endpoint"`
34+
TokenEndpoint string `json:"token_endpoint"`
35+
UserInfo string `json:"userinfo_endpoint"`
36+
}
37+
if err := json.NewDecoder(resp.Body).Decode(&cfg); err != nil {
38+
return nil, err
39+
}
40+
r, err := url.JoinPath(externalURL, OIDCCallbackEndpoint)
41+
if err != nil {
42+
return nil, err
43+
}
44+
return &oidcProvider{
45+
oauth2: oauth2.Config{
46+
ClientID: clientID,
47+
ClientSecret: clientSecret,
48+
RedirectURL: r,
49+
Scopes: scopes,
50+
Endpoint: oauth2.Endpoint{
51+
AuthURL: cfg.AuthEndpoint,
52+
TokenURL: cfg.TokenEndpoint,
53+
},
54+
},
55+
providerName: providerName,
56+
userInfoURL: cfg.UserInfo,
57+
userIDField: userIDField,
58+
allowedUsers: allowedUsers,
59+
}, nil
60+
}
61+
62+
func (p *oidcProvider) Name() string {
63+
return p.providerName
64+
}
65+
66+
func (p *oidcProvider) RedirectURL() string {
67+
return OIDCCallbackEndpoint
68+
}
69+
70+
func (p *oidcProvider) AuthURL() string {
71+
return OIDCAuthEndpoint
72+
}
73+
74+
func (p *oidcProvider) AuthCodeURL(c *gin.Context, state string) (string, error) {
75+
authURL := p.oauth2.AuthCodeURL(state)
76+
return authURL, nil
77+
}
78+
79+
func (p *oidcProvider) Exchange(c *gin.Context, state string) (*oauth2.Token, error) {
80+
if c.Query("state") != state {
81+
return nil, errors.New("invalid OAuth state")
82+
}
83+
code := c.Query("code")
84+
token, err := p.oauth2.Exchange(c, code)
85+
if err != nil {
86+
return nil, err
87+
}
88+
return token, nil
89+
}
90+
91+
func (p *oidcProvider) GetUserID(ctx context.Context, token *oauth2.Token) (string, error) {
92+
client := p.oauth2.Client(ctx, token)
93+
resp, err := client.Get(p.userInfoURL)
94+
if err != nil {
95+
return "", err
96+
}
97+
defer resp.Body.Close()
98+
var obj any
99+
if err := json.NewDecoder(resp.Body).Decode(&obj); err != nil {
100+
return "", err
101+
}
102+
v, err := jsonpointer.Get(obj, p.userIDField)
103+
if err != nil {
104+
return "", err
105+
}
106+
userID, ok := v.(string)
107+
if !ok {
108+
return "", errors.New("user ID field is not a string")
109+
}
110+
return userID, nil
111+
}
112+
113+
func (p *oidcProvider) Authorization(userid string) (bool, error) {
114+
if len(p.allowedUsers) == 0 {
115+
return true, nil
116+
}
117+
for _, allowedUser := range p.allowedUsers {
118+
if allowedUser == userid {
119+
return true, nil
120+
}
121+
}
122+
return false, nil
123+
}

pkg/mcp-proxy/main.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ func Run(
4848
githubClientID string,
4949
githubClientSecret string,
5050
githubAllowedUsers []string,
51+
oidcConfigurationURL string,
52+
oidcClientID string,
53+
oidcClientSecret string,
54+
oidcScopes []string,
55+
oidcUserIDField string,
56+
oidcProviderName string,
57+
oidcAllowedUsers []string,
5158
password string,
5259
passwordHash string,
5360
proxyHeaders []string,
@@ -148,6 +155,24 @@ func Run(
148155
providers = append(providers, githubProvider)
149156
}
150157

158+
// Add OIDC provider if configured
159+
if oidcConfigurationURL != "" && oidcClientID != "" && oidcClientSecret != "" {
160+
oidcProvider, err := auth.NewOIDCProvider(
161+
oidcConfigurationURL,
162+
oidcScopes,
163+
oidcUserIDField,
164+
oidcProviderName,
165+
externalURL,
166+
oidcClientID,
167+
oidcClientSecret,
168+
oidcAllowedUsers,
169+
)
170+
if err != nil {
171+
return fmt.Errorf("failed to create OIDC provider: %w", err)
172+
}
173+
providers = append(providers, oidcProvider)
174+
}
175+
151176
var passwordHashes []string
152177

153178
// Handle password argument - generate bcrypt hash if provided

0 commit comments

Comments
 (0)