Skip to content

Commit 2419ef5

Browse files
authored
Merge pull request #2 from sigbit/add-password-auth
Add password auth
2 parents 9c6e916 + dd65e7e commit 2419ef5

6 files changed

Lines changed: 287 additions & 85 deletions

File tree

README.md

Lines changed: 49 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,29 @@ docker run --rm -p 8081:8081 --net=host \
1111
-e EXTERNAL_URL=http://localhost:8081 \
1212
-e PROXY_URL=http://localhost:8080 \
1313
-e GLOBAL_SECRET=$(openssl rand -hex 32) \
14-
-e GOOGLE_CLIENT_ID=... \
15-
-e GOOGLE_CLIENT_SECRET=... \
16-
-e GOOGLE_ALLOWED_USERS=... \
14+
-e PASSWORD=changeme \
15+
-v ./data:/data \
1716
ghcr.io/sigbit/mcp-auth-proxy:latest
1817
```
1918

19+
.mcp.json
20+
```json
21+
{
22+
"mcpServers": {
23+
"mcp": {
24+
"type": "http",
25+
"url": "http://localhost:8081/mcp"
26+
}
27+
}
28+
}
29+
```
30+
31+
2032
## Overview
2133

2234
MCP Auth Proxy is a secure OAuth 2.1 authentication proxy for Model Context Protocol (MCP) servers. MCP servers are expected to support not only standard OAuth 2.1 flows but also Dynamic Client support (e.g., dynamic client registration) and authentication-related .well-known metadata. On top of that, different MCP clients handle tokens differently, which makes implementation tricky.
2335

24-
MCP Auth Proxy sits in front of your MCP services and enforces sign-in with OAuth providers (such as Google or GitHub) before users can access protected MCP resources.
36+
MCP Auth Proxy sits in front of your MCP services and enforces sign-in with OAuth providers (such as Google or GitHub) or password before users can access protected MCP resources.
2537

2638
## Note
2739

@@ -38,16 +50,16 @@ For a simpler approach to publish local MCP servers over OAuth, consider [MCP Wa
3850
| `EXTERNAL_URL` | No | External URL for OAuth callbacks | `http://localhost:8081` |
3951
| `PROXY_URL` | No | Target MCP server URL | `http://localhost:8080` |
4052
| `GLOBAL_SECRET` | No | Global secret for session encryption | `supersecret` |
41-
| `GOOGLE_CLIENT_ID` | No* | Google OAuth client ID | - |
42-
| `GOOGLE_CLIENT_SECRET` | No* | Google OAuth client secret | - |
43-
| `GOOGLE_ALLOWED_USERS` | No* | Comma-separated list of allowed Google emails | - |
44-
| `GITHUB_CLIENT_ID` | No* | GitHub OAuth client ID | - |
45-
| `GITHUB_CLIENT_SECRET` | No* | GitHub OAuth client secret | - |
46-
| `GITHUB_ALLOWED_USERS` | No* | Comma-separated list of allowed GitHub usernames | - |
53+
| `GOOGLE_CLIENT_ID` | No | Google OAuth client ID | - |
54+
| `GOOGLE_CLIENT_SECRET` | No | Google OAuth client secret | - |
55+
| `GOOGLE_ALLOWED_USERS` | No | Comma-separated list of allowed Google emails | - |
56+
| `GITHUB_CLIENT_ID` | No | GitHub OAuth client ID | - |
57+
| `GITHUB_CLIENT_SECRET` | No | GitHub OAuth client secret | - |
58+
| `GITHUB_ALLOWED_USERS` | No | Comma-separated list of allowed GitHub usernames | - |
59+
| `PASSWORD` | No | Password for authentication | - |
60+
| `PASSWORD_HASH` | No | Hash of the password for authentication | - |
4761
| `MODE` | No | Set to `debug` for development mode | `production` |
4862

49-
*At least one OAuth provider must be configured (Google or GitHub)
50-
5163
### OAuth Provider Setup
5264

5365
#### Google OAuth Setup
@@ -61,57 +73,40 @@ For a simpler approach to publish local MCP servers over OAuth, consider [MCP Wa
6173
1. Go to the [Register new GitHub App](https://github.com/settings/apps/new)
6274
2. Set Authorization callback URL: `{EXTERNAL_URL}/.auth/github/callback`
6375

64-
## 🚀 Installation & Usage
76+
## 🚀 Usage
6577

66-
### Method 1: Direct Binary
78+
### Method 1: Download Binary
6779

68-
```bash
69-
# Clone the repository
70-
git clone https://github.com/sigbit/mcp-auth-proxy.git
71-
cd mcp-auth-proxy
72-
73-
# Build the binary
74-
go build -o mcp-auth-proxy .
75-
76-
# Run with environment variables
77-
export GOOGLE_CLIENT_ID="your-google-client-id"
78-
export GOOGLE_CLIENT_SECRET="your-google-client-secret"
79-
export GOOGLE_ALLOWED_USERS="[email protected],[email protected]"
80-
./mcp-auth-proxy
81-
```
82-
83-
### Method 2: Command Line Arguments
80+
Download the latest binary from [releases](https://github.com/sigbit/mcp-auth-proxy/releases) and run with command line options:
8481

8582
```bash
8683
./mcp-auth-proxy \
87-
--external-url "https://your-domain.com" \
88-
--proxy-url "http://your-mcp-server:8080" \
84+
--external-url "http://localhost:8081" \
85+
--proxy-url "http://localhost:8080" \
86+
--global-secret "$(openssl rand -hex 32)" \
8987
--google-client-id "your-google-client-id" \
9088
--google-client-secret "your-google-client-secret" \
91-
--google-allowed-users "[email protected],[email protected]"
89+
--google-allowed-users "[email protected],[email protected]" \
90+
--github-client-id "your-github-client-id" \
91+
--github-client-secret "your-github-client-secret" \
92+
--github-allowed-users "username1,username2" \
93+
--password "changeme"
9294
```
9395

94-
### Method 3: Docker Compose (Recommended)
95-
96-
1. Create a `.env` file in the `example/` directory:
97-
98-
```env
99-
GLOBAL_SECRET=your-super-secret-key-here
100-
GOOGLE_CLIENT_ID=your-google-client-id
101-
GOOGLE_CLIENT_SECRET=your-google-client-secret
102-
103-
GITHUB_CLIENT_ID=your-github-client-id
104-
GITHUB_CLIENT_SECRET=your-github-client-secret
105-
GITHUB_ALLOWED_USERS=username1,username2
106-
```
107-
108-
2. Run with Docker Compose:
96+
### Method 2: Docker
10997

11098
```bash
111-
cd example/
112-
docker compose up -d
99+
docker run --rm -p 8081:8081 --net=host \
100+
-e EXTERNAL_URL=http://localhost:8081 \
101+
-e PROXY_URL=http://localhost:8080 \
102+
-e GLOBAL_SECRET=$(openssl rand -hex 32) \
103+
-e GOOGLE_CLIENT_ID="your-google-client-id" \
104+
-e GOOGLE_CLIENT_SECRET="your-google-client-secret" \
105+
-e GOOGLE_ALLOWED_USERS="[email protected],[email protected]" \
106+
-e GITHUB_CLIENT_ID="your-github-client-id" \
107+
-e GITHUB_CLIENT_SECRET="your-github-client-secret" \
108+
-e GITHUB_ALLOWED_USERS="username1,username2" \
109+
-e PASSWORD=changeme \
110+
-v ./data:/data \
111+
ghcr.io/sigbit/mcp-auth-proxy:latest
113112
```
114-
115-
This will start:
116-
- MCP Auth Proxy on port 8081
117-
- Playwright MCP server on port 8931 (as an example backend)

example/.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# for google oauth Authentication
12
GOOGLE_CLIENT_ID=**********************
23
GOOGLE_CLIENT_SECRET=**********************
34
5+
# for Password Authentication
6+
PASSWORD=changeme

main.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ func main() {
2727
var githubClientID string
2828
var githubClientSecret string
2929
var githubAllowedUsers string
30+
var password string
31+
var passwordHash string
3032

3133
rootCmd := &cobra.Command{
3234
Use: "mcp-warp",
@@ -59,6 +61,8 @@ func main() {
5961
githubClientID,
6062
githubClientSecret,
6163
githubAllowedUsersList,
64+
password,
65+
passwordHash,
6266
); err != nil {
6367
panic(err)
6468
}
@@ -70,17 +74,21 @@ func main() {
7074
rootCmd.Flags().StringVarP(&externalURL, "external-url", "e", getEnvWithDefault("EXTERNAL_URL", "http://localhost:8081"), "External URL for the proxy")
7175
rootCmd.Flags().StringVarP(&proxyURL, "proxy-url", "p", getEnvWithDefault("PROXY_URL", "http://localhost:8080"), "Proxy URL for the proxy")
7276
rootCmd.Flags().StringVarP(&globalSecret, "global-secret", "s", getEnvWithDefault("GLOBAL_SECRET", "supersecret"), "Global secret for the proxy")
73-
77+
7478
// Google OAuth configuration
7579
rootCmd.Flags().StringVar(&googleClientID, "google-client-id", getEnvWithDefault("GOOGLE_CLIENT_ID", ""), "Google OAuth client ID")
7680
rootCmd.Flags().StringVar(&googleClientSecret, "google-client-secret", getEnvWithDefault("GOOGLE_CLIENT_SECRET", ""), "Google OAuth client secret")
7781
rootCmd.Flags().StringVar(&googleAllowedUsers, "google-allowed-users", getEnvWithDefault("GOOGLE_ALLOWED_USERS", ""), "Comma-separated list of allowed Google users (emails)")
78-
82+
7983
// GitHub OAuth configuration
8084
rootCmd.Flags().StringVar(&githubClientID, "github-client-id", getEnvWithDefault("GITHUB_CLIENT_ID", ""), "GitHub OAuth client ID")
8185
rootCmd.Flags().StringVar(&githubClientSecret, "github-client-secret", getEnvWithDefault("GITHUB_CLIENT_SECRET", ""), "GitHub OAuth client secret")
8286
rootCmd.Flags().StringVar(&githubAllowedUsers, "github-allowed-users", getEnvWithDefault("GITHUB_ALLOWED_USERS", ""), "Comma-separated list of allowed GitHub users (usernames)")
8387

88+
// Password authentication
89+
rootCmd.Flags().StringVar(&password, "password", getEnvWithDefault("PASSWORD", ""), "Plain text password for authentication (will be hashed with bcrypt)")
90+
rootCmd.Flags().StringVar(&passwordHash, "password-hash", getEnvWithDefault("PASSWORD_HASH", ""), "Bcrypt hash of password for authentication")
91+
8492
if err := rootCmd.Execute(); err != nil {
8593
panic(err)
8694
}

pkg/auth/main.go

Lines changed: 94 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/gin-contrib/sessions"
1010
"github.com/gin-gonic/gin"
11+
"golang.org/x/crypto/bcrypt"
1112
"golang.org/x/oauth2"
1213
)
1314

@@ -25,12 +26,13 @@ type Provider interface {
2526
}
2627

2728
type AuthRouter struct {
28-
providers map[string]Provider
29-
template *template.Template
29+
passwordHash []string
30+
providers map[string]Provider
31+
template *template.Template
3032
unauthorizedTemplate *template.Template
3133
}
3234

33-
func NewAuthRouter(providers ...Provider) (*AuthRouter, error) {
35+
func NewAuthRouter(passwordHash []string, providers ...Provider) (*AuthRouter, error) {
3436
providersMap := make(map[string]Provider)
3537
for _, provider := range providers {
3638
providersMap[provider.Name()] = provider
@@ -40,15 +42,16 @@ func NewAuthRouter(providers ...Provider) (*AuthRouter, error) {
4042
if err != nil {
4143
return nil, err
4244
}
43-
45+
4446
unauthorizedTmpl, err := template.ParseFS(templateFS, "templates/unauthorized.html")
4547
if err != nil {
4648
return nil, err
4749
}
4850

4951
return &AuthRouter{
50-
providers: providersMap,
51-
template: tmpl,
52+
passwordHash: passwordHash,
53+
providers: providersMap,
54+
template: tmpl,
5255
unauthorizedTemplate: unauthorizedTmpl,
5356
}, nil
5457
}
@@ -60,10 +63,14 @@ const (
6063
GoogleCallbackEndpoint = "/.auth/google/callback"
6164
GitHubAuthEndpoint = "/.auth/github"
6265
GitHubCallbackEndpoint = "/.auth/github/callback"
66+
67+
PasswordProvider = "password"
68+
PasswordUserID = "password_user"
6369
)
6470

6571
func (a *AuthRouter) SetupRoutes(router gin.IRouter) {
6672
router.GET(LoginEndpoint, a.handleLogin)
73+
router.POST(LoginEndpoint, a.handleLoginPost)
6774
router.GET(LogoutEndpoint, a.handleLogout)
6875
for providerName, provider := range a.providers {
6976
router.GET(provider.RedirectURL(), func(c *gin.Context) {
@@ -103,6 +110,11 @@ type ProviderData struct {
103110
}
104111

105112
func (a *AuthRouter) handleLogin(c *gin.Context) {
113+
if c.Request.Method == "POST" {
114+
a.handleLoginPost(c)
115+
return
116+
}
117+
106118
var providersData []ProviderData
107119
for name := range a.providers {
108120
providersData = append(providersData, ProviderData{
@@ -112,9 +124,13 @@ func (a *AuthRouter) handleLogin(c *gin.Context) {
112124
}
113125

114126
data := struct {
115-
Providers []ProviderData
127+
Providers []ProviderData
128+
HasPassword bool
129+
PasswordError string
116130
}{
117-
Providers: providersData,
131+
Providers: providersData,
132+
HasPassword: len(a.passwordHash) > 0,
133+
PasswordError: "",
118134
}
119135

120136
c.Header("Content-Type", "text/html; charset=utf-8")
@@ -124,11 +140,73 @@ func (a *AuthRouter) handleLogin(c *gin.Context) {
124140
}
125141
}
126142

143+
func (a *AuthRouter) handleLoginPost(c *gin.Context) {
144+
password := c.PostForm("password")
145+
var errorMessage string
146+
147+
if password == "" {
148+
errorMessage = "Password is required"
149+
} else {
150+
var isValid bool
151+
for _, hash := range a.passwordHash {
152+
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
153+
if err == nil {
154+
isValid = true
155+
break
156+
}
157+
}
158+
159+
if !isValid {
160+
errorMessage = "Invalid password"
161+
}
162+
}
163+
164+
if errorMessage != "" {
165+
var providersData []ProviderData
166+
for name := range a.providers {
167+
providersData = append(providersData, ProviderData{
168+
Name: name,
169+
URL: a.providers[name].AuthURL(),
170+
})
171+
}
172+
173+
data := struct {
174+
Providers []ProviderData
175+
HasPassword bool
176+
PasswordError string
177+
}{
178+
Providers: providersData,
179+
HasPassword: len(a.passwordHash) > 0,
180+
PasswordError: errorMessage,
181+
}
182+
183+
c.Header("Content-Type", "text/html; charset=utf-8")
184+
c.Status(http.StatusBadRequest)
185+
if err := a.template.Execute(c.Writer, data); err != nil {
186+
c.AbortWithError(http.StatusInternalServerError, err)
187+
return
188+
}
189+
return
190+
}
191+
192+
session := sessions.Default(c)
193+
session.Set("provider", PasswordProvider)
194+
session.Set("user_id", PasswordUserID)
195+
session.Save()
196+
197+
redirectURL := session.Get("redirect_url")
198+
if redirectURL == nil {
199+
c.Redirect(http.StatusFound, "/")
200+
return
201+
}
202+
c.Redirect(http.StatusFound, redirectURL.(string))
203+
}
204+
127205
func (a *AuthRouter) handleLogout(c *gin.Context) {
128206
session := sessions.Default(c)
129207
session.Clear()
130208
session.Save()
131-
c.Redirect(http.StatusFound, LoginEndpoint)
209+
c.String(http.StatusOK, "Logged out")
132210
}
133211

134212
func (a *AuthRouter) RequireAuth() gin.HandlerFunc {
@@ -142,6 +220,13 @@ func (a *AuthRouter) RequireAuth() gin.HandlerFunc {
142220
c.Redirect(http.StatusFound, LoginEndpoint)
143221
return
144222
}
223+
224+
// Allow password authentication
225+
if providerName.(string) == PasswordProvider {
226+
c.Next()
227+
return
228+
}
229+
145230
p, ok := a.providers[providerName.(string)]
146231
if !ok {
147232
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unknown provider"})

0 commit comments

Comments
 (0)