Skip to content

Commit fe65156

Browse files
authored
feat: support glob patterns for OIDC allowed users (#77)
* feat(auth/oidc): add glob pattern support for allowed users - Add OIDC_ALLOWED_USERS_GLOB flag/env to allow user authorization via glob patterns - Compile and evaluate patterns with github.com/gobwas/glob - Preserve exact match checks (OIDC_ALLOWED_USERS) and fall back to globs - Introduce splitWithEscapes to parse comma-separated values with escaped delimiters - Wire through CLI flags and pkg/mcp-proxy - Add tests for glob matching and helper parsing - Update README and docs to document new option Backward compatible: when no exact or glob rules are set, all users are allowed (as before). * build: promote github.com/gobwas/glob to direct dependency Also tidy OIDC glob pattern test formatting.
1 parent 2dc1629 commit fe65156

11 files changed

Lines changed: 430 additions & 21 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88

99
- **Drop-in OAuth 2.1/OIDC gateway for MCP servers — put it in front, no code changes.**
1010
- **Your IdP, your choice**: Google, GitHub, or any OIDC provider — e.g. Okta, Auth0, Azure AD, Keycloak — plus optional password.
11-
- **Publish local MCP servers safely**: Supports all stdio, SSE, and HTTP transports. For stdio, traffic is converted to `/mcp`. For SSE/HTTP, it’s proxied as-is. Of course, with authentication.
11+
- **Flexible user matching**: Support exact matching and glob patterns for user authorization (e.g., `*@company.com`)
12+
- **Publish local MCP servers safely**: Supports all stdio, SSE, and HTTP transports. For stdio, traffic is converted to `/mcp`. For SSE/HTTP, it's proxied as-is. Of course, with authentication.
1213
- **Verified across major MCP clients**: Claude, Claude Code, ChatGPT, GitHub Copilot, Cursor, etc. — the proxy smooths client-specific quirks for consistent auth.
1314

1415
---

docs/docs/configuration.md

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,37 @@ Complete reference for all MCP Auth Proxy configuration options.
5252

5353
#### Generic OIDC
5454

55-
| Option | Environment Variable | Default | Description |
56-
| -------------------------- | ------------------------ | ---------------------- | ----------------------------------------------------------- |
57-
| `--oidc-configuration-url` | `OIDC_CONFIGURATION_URL` | - | OIDC configuration URL |
58-
| `--oidc-client-id` | `OIDC_CLIENT_ID` | - | OIDC client ID |
59-
| `--oidc-client-secret` | `OIDC_CLIENT_SECRET` | - | OIDC client secret |
60-
| `--oidc-allowed-users` | `OIDC_ALLOWED_USERS` | - | Comma-separated list of allowed OIDC users |
61-
| `--oidc-provider-name` | `OIDC_PROVIDER_NAME` | `OIDC` | Display name for OIDC provider |
62-
| `--oidc-scopes` | `OIDC_SCOPES` | `openid,profile,email` | Comma-separated list of OIDC scopes |
63-
| `--oidc-user-id-field` | `OIDC_USER_ID_FIELD` | `/email` | JSON pointer to user ID field in userinfo endpoint response |
55+
| Option | Environment Variable | Default | Description |
56+
| --------------------------- | ------------------------- | ---------------------- | ------------------------------------------------------------ |
57+
| `--oidc-configuration-url` | `OIDC_CONFIGURATION_URL` | - | OIDC configuration URL |
58+
| `--oidc-client-id` | `OIDC_CLIENT_ID` | - | OIDC client ID |
59+
| `--oidc-client-secret` | `OIDC_CLIENT_SECRET` | - | OIDC client secret |
60+
| `--oidc-allowed-users` | `OIDC_ALLOWED_USERS` | - | Comma-separated list of allowed OIDC users (exact match) |
61+
| `--oidc-allowed-users-glob` | `OIDC_ALLOWED_USERS_GLOB` | - | Comma-separated list of glob patterns for allowed OIDC users |
62+
| `--oidc-provider-name` | `OIDC_PROVIDER_NAME` | `OIDC` | Display name for OIDC provider |
63+
| `--oidc-scopes` | `OIDC_SCOPES` | `openid,profile,email` | Comma-separated list of OIDC scopes |
64+
| `--oidc-user-id-field` | `OIDC_USER_ID_FIELD` | `/email` | JSON pointer to user ID field in userinfo endpoint response |
65+
66+
##### OIDC User Matching
67+
68+
You can use both exact matching and glob patterns for OIDC user authorization:
69+
70+
- **Exact matching** (`--oidc-allowed-users`): Users must match exactly
71+
- **Glob patterns** (`--oidc-allowed-users-glob`): Users are matched against [glob patterns](https://github.com/gobwas/glob)
72+
73+
**Examples:**
74+
75+
```bash
76+
# Exact matching
77+
--oidc-allowed-users "[email protected],[email protected]"
78+
79+
# Glob patterns - allow all users from example.com
80+
--oidc-allowed-users-glob "*@example.com"
81+
82+
# Combined exact and glob matching
83+
--oidc-allowed-users "[email protected]" \
84+
--oidc-allowed-users-glob "*@example.com"
85+
```
6486

6587
### Server Options
6688

docs/docs/examples.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ services:
4242
- GITHUB_CLIENT_SECRET=your-github-client-secret
4343
- GITHUB_ALLOWED_USERS=username1,username2
4444
- GITHUB_ALLOWED_ORGS=org1,org2:team1
45+
- OIDC_CONFIGURATION_URL=https://your-oidc-provider/.well-known/openid_configuration
46+
- OIDC_CLIENT_ID=your-oidc-client-id
47+
- OIDC_CLIENT_SECRET=your-oidc-client-secret
48+
49+
- OIDC_ALLOWED_USERS_GLOB=*@example.com
4550
- TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
4651
volumes:
4752
- ./data:/data

docs/docs/oauth-setup.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ Configure OAuth providers to enable secure authentication for your MCP server.
9999

100100
### 2. Configure MCP Auth Proxy
101101

102+
#### Exact user matching:
103+
102104
```bash
103105
./mcp-auth-proxy \
104106
--external-url https://{your-domain} \
@@ -110,6 +112,19 @@ Configure OAuth providers to enable secure authentication for your MCP server.
110112
-- your-mcp-command
111113
```
112114

115+
#### Glob pattern matching:
116+
117+
```bash
118+
./mcp-auth-proxy \
119+
--external-url https://{your-domain} \
120+
--tls-accept-tos \
121+
--oidc-configuration-url "https://your-provider.com/.well-known/openid-configuration" \
122+
--oidc-client-id "your-oidc-client-id" \
123+
--oidc-client-secret "your-oidc-client-secret" \
124+
--oidc-allowed-users-glob "*@example.com" \
125+
-- your-mcp-command
126+
```
127+
113128
### Provider-Specific Examples
114129

115130
#### Okta
@@ -167,6 +182,7 @@ export OIDC_CONFIGURATION_URL="https://provider.com/.well-known/openid-configura
167182
export OIDC_CLIENT_ID="your-oidc-client-id"
168183
export OIDC_CLIENT_SECRET="your-oidc-client-secret"
169184
export OIDC_ALLOWED_USERS="[email protected],[email protected]"
185+
export OIDC_ALLOWED_USERS_GLOB="*@example.com"
170186

171187
./mcp-auth-proxy --external-url https://{your-domain} --tls-accept-tos -- your-mcp-command
172188
```

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ require (
77
github.com/gin-contrib/sessions v1.0.4
88
github.com/gin-contrib/zap v1.1.5
99
github.com/gin-gonic/gin v1.10.1
10+
github.com/gobwas/glob v0.2.3
1011
github.com/golang-jwt/jwt/v5 v5.3.0
1112
github.com/mark3labs/mcp-go v0.37.0
1213
github.com/mattn/go-jsonpointer v0.0.1

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ github.com/gobuffalo/pop/v6 v6.1.1 h1:eUDBaZcb0gYrmFnKwpuTEUA7t5ZHqNfvS4POqJYXDZ
155155
github.com/gobuffalo/pop/v6 v6.1.1/go.mod h1:1n7jAmI1i7fxuXPZjZb0VBPQDbksRtCoFnrDV5IsvaI=
156156
github.com/gobuffalo/tags/v3 v3.1.4/go.mod h1:ArRNo3ErlHO8BtdA0REaZxijuWnWzF6PUXngmMXd2I0=
157157
github.com/gobuffalo/validate/v3 v3.3.3/go.mod h1:YC7FsbJ/9hW/VjQdmXPvFqvRis4vrRYFxr69WiNZw6g=
158+
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
159+
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
158160
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
159161
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
160162
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=

main.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,45 @@ func getEnvBoolWithDefault(key string, defaultValue bool) bool {
2525
return defaultValue
2626
}
2727

28+
// splitWithEscapes splits a string by delimiter, respecting escape sequences
29+
// e.g., "a,b\,c,d" with delimiter "," returns ["a", "b,c", "d"]
30+
func splitWithEscapes(s, delimiter string) []string {
31+
if s == "" {
32+
return []string{}
33+
}
34+
35+
var result []string
36+
var current strings.Builder
37+
escaped := false
38+
39+
for i := 0; i < len(s); i++ {
40+
if escaped {
41+
current.WriteByte(s[i])
42+
escaped = false
43+
} else if s[i] == '\\' && i+1 < len(s) {
44+
// Check if next character is the delimiter
45+
if strings.HasPrefix(s[i+1:], delimiter) {
46+
// Skip the backslash and add the delimiter character
47+
escaped = true
48+
} else {
49+
// Not escaping delimiter, keep the backslash
50+
current.WriteByte(s[i])
51+
}
52+
} else if strings.HasPrefix(s[i:], delimiter) {
53+
// Found unescaped delimiter
54+
result = append(result, strings.TrimSpace(current.String()))
55+
current.Reset()
56+
i += len(delimiter) - 1 // -1 because loop will increment
57+
} else {
58+
current.WriteByte(s[i])
59+
}
60+
}
61+
62+
// Add the last part
63+
result = append(result, strings.TrimSpace(current.String()))
64+
return result
65+
}
66+
2867
func main() {
2968
var listen string
3069
var tlsListen string
@@ -49,6 +88,7 @@ func main() {
4988
var oidcUserIDField string
5089
var oidcProviderName string
5190
var oidcAllowedUsers string
91+
var oidcAllowedUsersGlob string
5292
var password string
5393
var passwordHash string
5494
var proxyBearerToken string
@@ -98,6 +138,11 @@ func main() {
98138
}
99139
}
100140

141+
var oidcAllowedUsersGlobList []string
142+
if oidcAllowedUsersGlob != "" {
143+
oidcAllowedUsersGlobList = splitWithEscapes(oidcAllowedUsersGlob, ",")
144+
}
145+
101146
var oidcScopesList []string
102147
if oidcScopes != "" {
103148
oidcScopesList = strings.Split(oidcScopes, ",")
@@ -149,6 +194,7 @@ func main() {
149194
oidcUserIDField,
150195
oidcProviderName,
151196
oidcAllowedUsersList,
197+
oidcAllowedUsersGlobList,
152198
password,
153199
passwordHash,
154200
trustedProxiesList,
@@ -190,6 +236,7 @@ func main() {
190236
rootCmd.Flags().StringVar(&oidcUserIDField, "oidc-user-id-field", getEnvWithDefault("OIDC_USER_ID_FIELD", "/email"), "JSON pointer to user ID field in userinfo endpoint response")
191237
rootCmd.Flags().StringVar(&oidcProviderName, "oidc-provider-name", getEnvWithDefault("OIDC_PROVIDER_NAME", "OIDC"), "Display name for OIDC provider")
192238
rootCmd.Flags().StringVar(&oidcAllowedUsers, "oidc-allowed-users", getEnvWithDefault("OIDC_ALLOWED_USERS", ""), "Comma-separated list of allowed OIDC users")
239+
rootCmd.Flags().StringVar(&oidcAllowedUsersGlob, "oidc-allowed-users-glob", getEnvWithDefault("OIDC_ALLOWED_USERS_GLOB", ""), "Comma-separated list of glob patterns for allowed OIDC users")
193240

194241
// Password authentication
195242
rootCmd.Flags().StringVar(&password, "password", getEnvWithDefault("PASSWORD", ""), "Plain text password for authentication (will be hashed with bcrypt)")

0 commit comments

Comments
 (0)