Skip to content

Commit 128e1cc

Browse files
authored
feat(auth): add no-provider-auto-select flag to disable auto-redirect (#80)
- Add noProviderAutoSelect to AuthRouter and mcp-proxy Run - Skip auto-redirect to the sole provider when no password is set - Update docs and tests to cover behavior Notes: This adds a new parameter to exported constructors; call sites pass the flag.
1 parent 3a4baff commit 128e1cc

File tree

6 files changed

+117
-11
lines changed

6 files changed

+117
-11
lines changed

docs/docs/configuration.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@ Complete reference for all MCP Auth Proxy configuration options.
2727

2828
#### Password Authentication
2929

30-
| Option | Environment Variable | Default | Description |
31-
| ----------------- | -------------------- | ------- | ------------------------------------------------------------------- |
32-
| `--password` | `PASSWORD` | - | Plain text password for authentication (will be hashed with bcrypt) |
33-
| `--password-hash` | `PASSWORD_HASH` | - | Bcrypt hash of password for authentication |
30+
| Option | Environment Variable | Default | Description |
31+
| --------------------------- | ------------------------- | ------- | -------------------------------------------------------------------------------------------- |
32+
| `--no-provider-auto-select` | `NO_PROVIDER_AUTO_SELECT` | `false` | Disable auto-redirect when only one OAuth/OIDC provider is configured and no password is set |
33+
| `--password` | `PASSWORD` | - | Plain text password for authentication (will be hashed with bcrypt) |
34+
| `--password-hash` | `PASSWORD_HASH` | - | Bcrypt hash of password for authentication |
3435

3536
#### Google OAuth
3637

main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ func main() {
8989
var oidcProviderName string
9090
var oidcAllowedUsers string
9191
var oidcAllowedUsersGlob string
92+
var noProviderAutoSelect bool
9293
var password string
9394
var passwordHash string
9495
var proxyBearerToken string
@@ -195,6 +196,7 @@ func main() {
195196
oidcProviderName,
196197
oidcAllowedUsersList,
197198
oidcAllowedUsersGlobList,
199+
noProviderAutoSelect,
198200
password,
199201
passwordHash,
200202
trustedProxiesList,
@@ -239,6 +241,7 @@ func main() {
239241
rootCmd.Flags().StringVar(&oidcAllowedUsersGlob, "oidc-allowed-users-glob", getEnvWithDefault("OIDC_ALLOWED_USERS_GLOB", ""), "Comma-separated list of glob patterns for allowed OIDC users")
240242

241243
// Password authentication
244+
rootCmd.Flags().BoolVar(&noProviderAutoSelect, "no-provider-auto-select", getEnvBoolWithDefault("NO_PROVIDER_AUTO_SELECT", false), "Disable auto-redirect when only one OAuth/OIDC provider is configured and no password is set")
242245
rootCmd.Flags().StringVar(&password, "password", getEnvWithDefault("PASSWORD", ""), "Plain text password for authentication (will be hashed with bcrypt)")
243246
rootCmd.Flags().StringVar(&passwordHash, "password-hash", getEnvWithDefault("PASSWORD_HASH", ""), "Bcrypt hash of password for authentication")
244247

pkg/auth/auth.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@ type AuthRouter struct {
2121
loginTemplate *template.Template
2222
unauthorizedTemplate *template.Template
2323
errorTemplate *template.Template
24+
// When true, do not auto-redirect to the sole provider even if
25+
// there is only one provider and no password is set.
26+
noProviderAutoSelect bool
2427
}
2528

26-
func NewAuthRouter(passwordHash []string, providers ...Provider) (*AuthRouter, error) {
29+
func NewAuthRouter(passwordHash []string, noProviderAutoSelect bool, providers ...Provider) (*AuthRouter, error) {
2730
tmpl, err := template.ParseFS(templateFS, "templates/login.html")
2831
if err != nil {
2932
return nil, err
@@ -45,6 +48,7 @@ func NewAuthRouter(passwordHash []string, providers ...Provider) (*AuthRouter, e
4548
loginTemplate: tmpl,
4649
unauthorizedTemplate: unauthorizedTmpl,
4750
errorTemplate: errorTmpl,
51+
noProviderAutoSelect: noProviderAutoSelect,
4852
}, nil
4953
}
5054

@@ -137,6 +141,11 @@ func (a *AuthRouter) handleLogin(c *gin.Context) {
137141
a.handleLoginPost(c)
138142
return
139143
}
144+
// Auto-redirect to the sole provider if enabled and no password is set
145+
if !a.noProviderAutoSelect && len(a.passwordHash) == 0 && len(a.providers) == 1 {
146+
c.Redirect(http.StatusFound, a.providers[0].AuthURL())
147+
return
148+
}
140149
a.renderLogin(c, "")
141150
}
142151

pkg/auth/auth_test.go

Lines changed: 96 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ func TestAuthenticationFlow(t *testing.T) {
5353
mockProvider.EXPECT().AuthURL().Return("/.auth/test").AnyTimes()
5454
mockProvider.EXPECT().RedirectURL().Return("/.auth/test/callback").AnyTimes()
5555

56-
// Create AuthRouter
57-
authRouter, err := NewAuthRouter(nil, mockProvider)
56+
// Create AuthRouter (auto-select enabled by default)
57+
authRouter, err := NewAuthRouter(nil, false, mockProvider)
5858
require.NoError(t, err)
5959

6060
router := setupTestRouter(authRouter)
@@ -88,7 +88,7 @@ func TestAuthenticationFlow(t *testing.T) {
8888
mockProvider.EXPECT().Authorization(gomock.Any(), mockToken).Return(true, "authorized_user", nil)
8989

9090
// Create AuthRouter
91-
authRouter, err := NewAuthRouter(nil, mockProvider)
91+
authRouter, err := NewAuthRouter(nil, false, mockProvider)
9292
require.NoError(t, err)
9393

9494
router := setupTestRouter(authRouter)
@@ -149,7 +149,7 @@ func TestAuthenticationFlow(t *testing.T) {
149149
mockProvider.EXPECT().Authorization(gomock.Any(), mockToken).Return(false, "unauthorized_user", nil)
150150

151151
// Create AuthRouter
152-
authRouter, err := NewAuthRouter(nil, mockProvider)
152+
authRouter, err := NewAuthRouter(nil, false, mockProvider)
153153
require.NoError(t, err)
154154

155155
router := setupTestRouter(authRouter)
@@ -187,3 +187,95 @@ func TestAuthenticationFlow(t *testing.T) {
187187
require.Equal(t, "/.auth/login", location)
188188
})
189189
}
190+
191+
func TestLoginAutoRedirect(t *testing.T) {
192+
t.Run("Auto-redirects when single provider and no password", func(t *testing.T) {
193+
ctrl := gomock.NewController(t)
194+
defer ctrl.Finish()
195+
196+
mockProvider := NewMockProvider(ctrl)
197+
mockProvider.EXPECT().Name().Return("test").AnyTimes()
198+
mockProvider.EXPECT().Type().Return("test").AnyTimes()
199+
mockProvider.EXPECT().AuthURL().Return("/.auth/test").AnyTimes()
200+
mockProvider.EXPECT().RedirectURL().Return("/.auth/test/callback").AnyTimes()
201+
202+
authRouter, err := NewAuthRouter(nil, false, mockProvider)
203+
require.NoError(t, err)
204+
205+
router := gin.New()
206+
store := memstore.NewStore([]byte("test-secret"))
207+
router.Use(sessions.Sessions("session", store))
208+
authRouter.SetupRoutes(router)
209+
210+
server := httptest.NewServer(router)
211+
defer server.Close()
212+
213+
client := setupClient()
214+
resp, err := client.Get(server.URL + LoginEndpoint)
215+
require.NoError(t, err)
216+
defer resp.Body.Close()
217+
218+
require.Equal(t, http.StatusFound, resp.StatusCode)
219+
location := resp.Header.Get("Location")
220+
require.Equal(t, "/.auth/test", location)
221+
})
222+
223+
t.Run("Does not redirect when disabled", func(t *testing.T) {
224+
ctrl := gomock.NewController(t)
225+
defer ctrl.Finish()
226+
227+
mockProvider := NewMockProvider(ctrl)
228+
mockProvider.EXPECT().Name().Return("test").AnyTimes()
229+
mockProvider.EXPECT().Type().Return("test").AnyTimes()
230+
mockProvider.EXPECT().AuthURL().Return("/.auth/test").AnyTimes()
231+
mockProvider.EXPECT().RedirectURL().Return("/.auth/test/callback").AnyTimes()
232+
233+
authRouter, err := NewAuthRouter(nil, true, mockProvider)
234+
require.NoError(t, err)
235+
236+
router := gin.New()
237+
store := memstore.NewStore([]byte("test-secret"))
238+
router.Use(sessions.Sessions("session", store))
239+
authRouter.SetupRoutes(router)
240+
241+
server := httptest.NewServer(router)
242+
defer server.Close()
243+
244+
client := setupClient()
245+
resp, err := client.Get(server.URL + LoginEndpoint)
246+
require.NoError(t, err)
247+
defer resp.Body.Close()
248+
249+
require.Equal(t, http.StatusOK, resp.StatusCode)
250+
})
251+
252+
t.Run("Does not redirect when password configured", func(t *testing.T) {
253+
ctrl := gomock.NewController(t)
254+
defer ctrl.Finish()
255+
256+
mockProvider := NewMockProvider(ctrl)
257+
mockProvider.EXPECT().Name().Return("test").AnyTimes()
258+
mockProvider.EXPECT().Type().Return("test").AnyTimes()
259+
mockProvider.EXPECT().AuthURL().Return("/.auth/test").AnyTimes()
260+
mockProvider.EXPECT().RedirectURL().Return("/.auth/test/callback").AnyTimes()
261+
262+
// Non-empty passwordHash slice disables auto-select
263+
authRouter, err := NewAuthRouter([]string{"dummy"}, false, mockProvider)
264+
require.NoError(t, err)
265+
266+
router := gin.New()
267+
store := memstore.NewStore([]byte("test-secret"))
268+
router.Use(sessions.Sessions("session", store))
269+
authRouter.SetupRoutes(router)
270+
271+
server := httptest.NewServer(router)
272+
defer server.Close()
273+
274+
client := setupClient()
275+
resp, err := client.Get(server.URL + LoginEndpoint)
276+
require.NoError(t, err)
277+
defer resp.Body.Close()
278+
279+
require.Equal(t, http.StatusOK, resp.StatusCode)
280+
})
281+
}

pkg/idp/idp_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func setupTestServer(t *testing.T) (*httptest.Server, repository.Repository, str
6464
})
6565

6666
// Create auth router and IDP router
67-
authRouter, err := auth.NewAuthRouter([]string{})
67+
authRouter, err := auth.NewAuthRouter([]string{}, false)
6868
require.NoError(t, err)
6969

7070
logger, _ := zap.NewDevelopment()

pkg/mcp-proxy/main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ func Run(
5757
oidcProviderName string,
5858
oidcAllowedUsers []string,
5959
oidcAllowedUsersGlob []string,
60+
noProviderAutoSelect bool,
6061
password string,
6162
passwordHash string,
6263
trustedProxy []string,
@@ -201,7 +202,7 @@ func Run(
201202
passwordHashes = append(passwordHashes, passwordHash)
202203
}
203204

204-
authRouter, err := auth.NewAuthRouter(passwordHashes, providers...)
205+
authRouter, err := auth.NewAuthRouter(passwordHashes, noProviderAutoSelect, providers...)
205206
if err != nil {
206207
return fmt.Errorf("failed to create auth router: %w", err)
207208
}

0 commit comments

Comments
 (0)