diff --git a/framework/configstore/clientconfig.go b/framework/configstore/clientconfig.go
index 0f71c910224..0ff7eb72ccf 100644
--- a/framework/configstore/clientconfig.go
+++ b/framework/configstore/clientconfig.go
@@ -87,6 +87,7 @@ type ClientConfig struct {
MCPCodeModeBindingLevel string `json:"mcp_code_mode_binding_level"` // Code mode binding level: "server" or "tool"
MCPToolSyncInterval int `json:"mcp_tool_sync_interval"` // Global tool sync interval in minutes (default: 10, 0 = disabled)
MCPDisableAutoToolInject bool `json:"mcp_disable_auto_tool_inject"` // When true, MCP tools are not injected into requests by default
+ MCPEnableTempTokenAuth bool `json:"mcp_enable_temp_token_auth"` // When true, scoped temp tokens can authorize MCP per-user OAuth pages
HeaderFilterConfig *tables.GlobalHeaderFilterConfig `json:"header_filter_config,omitempty"` // Global header filtering configuration for x-bf-eh-* headers
AsyncJobResultTTL int `json:"async_job_result_ttl"` // Default TTL for async job results in seconds (default: 3600 = 1 hour)
RequiredHeaders []string `json:"required_headers,omitempty"` // Headers that must be present on every request (case-insensitive)
@@ -208,6 +209,11 @@ func (c *ClientConfig) GenerateClientConfigHash() (string, error) {
hash.Write([]byte("mcpDisableAutoToolInject:true"))
}
+ // Only hash non-default value to avoid legacy config hash churn on upgrade.
+ if c.MCPEnableTempTokenAuth {
+ hash.Write([]byte("mcpEnableTempTokenAuth:true"))
+ }
+
// Only hash non-default value to avoid legacy config hash churn on upgrade.
if c.AllowPerRequestContentStorageOverride {
hash.Write([]byte("allowPerRequestContentStorageOverride:true"))
diff --git a/framework/configstore/migrations.go b/framework/configstore/migrations.go
index bdde103d4e0..86cbd13b60a 100644
--- a/framework/configstore/migrations.go
+++ b/framework/configstore/migrations.go
@@ -636,6 +636,9 @@ func triggerMigrations(ctx context.Context, db *gorm.DB) error {
if err := migrationAddMCPDisableAutoToolInjectColumn(ctx, db); err != nil {
return err
}
+ if err := migrationAddMCPEnableTempTokenAuthColumn(ctx, db); err != nil {
+ return err
+ }
if err := migrationBackfillAllowedModelsWildcard(ctx, db); err != nil {
return err
}
@@ -1301,6 +1304,7 @@ func migrationDropAllowDirectKeysColumn(ctx context.Context, db *gorm.DB) error
MCPCodeModeBindingLevel: cc.MCPCodeModeBindingLevel,
MCPToolSyncInterval: cc.MCPToolSyncInterval,
MCPDisableAutoToolInject: cc.MCPDisableAutoToolInject,
+ MCPEnableTempTokenAuth: cc.MCPEnableTempTokenAuth,
HeaderFilterConfig: cc.HeaderFilterConfig,
AsyncJobResultTTL: cc.AsyncJobResultTTL,
RequiredHeaders: cc.RequiredHeaders,
@@ -5335,6 +5339,35 @@ func migrationAddMCPDisableAutoToolInjectColumn(ctx context.Context, db *gorm.DB
return nil
}
+// migrationAddMCPEnableTempTokenAuthColumn adds the mcp_enable_temp_token_auth column to the client config table.
+func migrationAddMCPEnableTempTokenAuthColumn(ctx context.Context, db *gorm.DB) error {
+ m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
+ ID: "add_mcp_enable_temp_token_auth_column",
+ Migrate: func(tx *gorm.DB) error {
+ tx = tx.WithContext(ctx)
+ migratorInstance := tx.Migrator()
+ if !migratorInstance.HasColumn(&tables.TableClientConfig{}, "mcp_enable_temp_token_auth") {
+ if err := migratorInstance.AddColumn(&tables.TableClientConfig{}, "mcp_enable_temp_token_auth"); err != nil {
+ return err
+ }
+ }
+ return nil
+ },
+ Rollback: func(tx *gorm.DB) error {
+ tx = tx.WithContext(ctx)
+ migratorInstance := tx.Migrator()
+ if err := migratorInstance.DropColumn(&tables.TableClientConfig{}, "mcp_enable_temp_token_auth"); err != nil {
+ return err
+ }
+ return nil
+ },
+ }})
+ if err := m.Migrate(); err != nil {
+ return fmt.Errorf("error while running mcp enable temp token auth migration: %s", err.Error())
+ }
+ return nil
+}
+
// migrationAddPricingRefactorColumns adds all new pricing columns introduced in the pricing module refactor
func migrationAddPricingRefactorColumns(ctx context.Context, db *gorm.DB) error {
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
@@ -6380,6 +6413,7 @@ func migrationAddRoutingChainMaxDepthColumn(ctx context.Context, db *gorm.DB) er
MCPCodeModeBindingLevel: cc.MCPCodeModeBindingLevel,
MCPToolSyncInterval: cc.MCPToolSyncInterval,
MCPDisableAutoToolInject: cc.MCPDisableAutoToolInject,
+ MCPEnableTempTokenAuth: cc.MCPEnableTempTokenAuth,
AsyncJobResultTTL: cc.AsyncJobResultTTL,
LoggingHeaders: cc.LoggingHeaders,
RequiredHeaders: cc.RequiredHeaders,
@@ -8303,6 +8337,7 @@ func migrationRefreshConfigHashAfterMCPExternalServerURLRemoval(ctx context.Cont
MCPCodeModeBindingLevel: cc.MCPCodeModeBindingLevel,
MCPToolSyncInterval: cc.MCPToolSyncInterval,
MCPDisableAutoToolInject: cc.MCPDisableAutoToolInject,
+ MCPEnableTempTokenAuth: cc.MCPEnableTempTokenAuth,
MCPExternalClientURL: schemas.NewEnvVar(cc.MCPExternalClientURL),
HeaderFilterConfig: cc.HeaderFilterConfig,
AsyncJobResultTTL: cc.AsyncJobResultTTL,
diff --git a/framework/configstore/rdb.go b/framework/configstore/rdb.go
index f104ed03287..54beed459ef 100644
--- a/framework/configstore/rdb.go
+++ b/framework/configstore/rdb.go
@@ -246,6 +246,7 @@ func (s *RDBConfigStore) UpdateClientConfig(ctx context.Context, config *ClientC
MCPCodeModeBindingLevel: config.MCPCodeModeBindingLevel,
MCPToolSyncInterval: config.MCPToolSyncInterval,
MCPDisableAutoToolInject: config.MCPDisableAutoToolInject,
+ MCPEnableTempTokenAuth: config.MCPEnableTempTokenAuth,
AsyncJobResultTTL: config.AsyncJobResultTTL,
RequiredHeaders: config.RequiredHeaders,
LoggingHeaders: config.LoggingHeaders,
@@ -508,6 +509,7 @@ func (s *RDBConfigStore) GetClientConfig(ctx context.Context) (*ClientConfig, er
MCPCodeModeBindingLevel: dbConfig.MCPCodeModeBindingLevel,
MCPToolSyncInterval: dbConfig.MCPToolSyncInterval,
MCPDisableAutoToolInject: dbConfig.MCPDisableAutoToolInject,
+ MCPEnableTempTokenAuth: dbConfig.MCPEnableTempTokenAuth,
AsyncJobResultTTL: dbConfig.AsyncJobResultTTL,
RequiredHeaders: dbConfig.RequiredHeaders,
LoggingHeaders: dbConfig.LoggingHeaders,
diff --git a/framework/configstore/tables/clientconfig.go b/framework/configstore/tables/clientconfig.go
index 68ee284f75a..03ff6b5a6bf 100644
--- a/framework/configstore/tables/clientconfig.go
+++ b/framework/configstore/tables/clientconfig.go
@@ -30,6 +30,7 @@ type TableClientConfig struct {
MCPCodeModeBindingLevel string `gorm:"default:server" json:"mcp_code_mode_binding_level"` // How tools are exposed in VFS: "server" or "tool"
MCPToolSyncInterval int `gorm:"default:10" json:"mcp_tool_sync_interval"` // Global tool sync interval in minutes (default: 10, 0 = disabled)
MCPDisableAutoToolInject bool `gorm:"default:false" json:"mcp_disable_auto_tool_inject"` // When true, MCP tools are not injected into requests by default
+ MCPEnableTempTokenAuth bool `gorm:"default:false" json:"mcp_enable_temp_token_auth"` // When true, scoped temp tokens can authorize MCP per-user OAuth pages
AsyncJobResultTTL int `gorm:"default:3600" json:"async_job_result_ttl"` // Default TTL for async job results in seconds (default: 3600 = 1 hour)
RequiredHeadersJSON string `gorm:"type:text" json:"-"` // JSON serialized []string
LoggingHeadersJSON string `gorm:"type:text" json:"-"` // JSON serialized []string
diff --git a/framework/oauth2/main.go b/framework/oauth2/main.go
index 45ca86c6cd2..1cf5231b53f 100644
--- a/framework/oauth2/main.go
+++ b/framework/oauth2/main.go
@@ -13,6 +13,7 @@ import (
"net/url"
"strings"
"sync"
+ "sync/atomic"
"time"
"github.com/google/uuid"
@@ -35,12 +36,17 @@ type OAuth2Provider struct {
mu sync.RWMutex
retryBaseDelay time.Duration // base delay for token endpoint retry backoff; doubles each attempt (1×, 2×, 4×)
- // tempTokens, when non-nil, is used by InitiateUserOAuthFlow to mint a
- // short-lived mcp_auth temp token and embed it in the returned auth-page
- // URL as a fragment. Optional — when nil, the URL is returned without a
- // fragment and the page works only for callers already authenticated to
- // the dashboard.
- tempTokens *temptoken.Service
+ // tempTokens, when non-nil and enabled in client config, is used by
+ // InitiateUserOAuthFlow to mint a short-lived mcp_auth temp token and
+ // embed it in the returned auth-page URL as a fragment. Optional — when
+ // nil or disabled, the URL is returned without a fragment and the page
+ // works only for callers already authenticated to the dashboard.
+ //
+ // Held as an atomic.Pointer rather than under p.mu: it is written once at
+ // startup and read on the request path, and p.mu is write-locked across
+ // token-refresh network I/O (RefreshAccessToken/RevokeToken). Sharing p.mu
+ // would stall flow init/cleanup reads behind unrelated refresh traffic.
+ tempTokens atomic.Pointer[temptoken.Service]
}
// NewOAuth2Provider creates a new OAuth provider instance
@@ -61,9 +67,27 @@ func NewOAuth2Provider(configStore configstore.ConfigStore, logger schemas.Logge
// have been constructed (the provider is built first by lib/config.go,
// the service later by the HTTP transport).
func (p *OAuth2Provider) SetTempTokenService(svc *temptoken.Service) {
- p.mu.Lock()
- defer p.mu.Unlock()
- p.tempTokens = svc
+ p.tempTokens.Store(svc)
+}
+
+// tempTokenService returns the current temp-token service. Lock-free: the
+// pointer is read atomically so request-path callers never contend with the
+// p.mu write lock held across token-refresh network I/O.
+func (p *OAuth2Provider) tempTokenService() *temptoken.Service {
+ return p.tempTokens.Load()
+}
+
+// mcpTempTokenAuthEnabled reports whether MCP per-user OAuth links may include temp-token auth.
+func (p *OAuth2Provider) mcpTempTokenAuthEnabled(ctx context.Context) bool {
+ if p.configStore == nil {
+ return false
+ }
+ clientConfig, err := p.configStore.GetClientConfig(ctx)
+ if err != nil {
+ logger.Warn("Failed to read MCP temp-token auth setting: %v", err)
+ return false
+ }
+ return clientConfig != nil && clientConfig.MCPEnableTempTokenAuth
}
// cleanupFlow deletes the flow row and any temp tokens minted for it. Called
@@ -80,8 +104,8 @@ func (p *OAuth2Provider) cleanupFlow(ctx context.Context, sessionID string) {
if err := p.configStore.DeleteOauthUserSession(cleanupCtx, sessionID); err != nil {
logger.Warn("per-user OAuth flow row cleanup failed: session_id=%s err=%v", sessionID, err)
}
- if p.tempTokens != nil {
- if _, err := p.tempTokens.DeleteByResourceID(cleanupCtx, temptoken.MCPAuthScopeName, sessionID); err != nil {
+ if svc := p.tempTokenService(); svc != nil {
+ if _, err := svc.DeleteByResourceID(cleanupCtx, temptoken.MCPAuthScopeName, sessionID); err != nil {
logger.Warn("per-user OAuth temp-token cleanup failed: session_id=%s err=%v", sessionID, err)
}
}
@@ -1029,10 +1053,10 @@ func (p *OAuth2Provider) InitiateUserOAuthFlow(ctx context.Context, oauthConfigI
// dashboard session can still call the per-user flow endpoints. The
// fragment never leaves the browser (not in server logs, not in the
// upstream-OAuth Referer), unlike a query param.
- if p.tempTokens != nil {
+ if svc := p.tempTokenService(); svc != nil && p.mcpTempTokenAuthEnabled(ctx) {
ttl := time.Until(expiresAt)
if ttl > 0 {
- plaintext, mintErr := p.tempTokens.Mint(ctx, temptoken.MCPAuthScopeName, sessionID, ttl)
+ plaintext, mintErr := svc.Mint(ctx, temptoken.MCPAuthScopeName, sessionID, ttl)
if mintErr != nil {
logger.Warn("Failed to mint mcp_auth temp token for flow %s: %v (link still usable for dashboard-authenticated callers)", sessionID, mintErr)
} else {
diff --git a/framework/oauth2/sync_test.go b/framework/oauth2/sync_test.go
index d3d5e0ae1f6..a1ab4021804 100644
--- a/framework/oauth2/sync_test.go
+++ b/framework/oauth2/sync_test.go
@@ -26,6 +26,7 @@ type testConfigStore struct {
mu sync.Mutex
oauthConfigs map[string]*tables.TableOauthConfig
oauthTokens map[string]*tables.TableOauthToken
+ clientConfig *configstore.ClientConfig
}
func newTestConfigStore() *testConfigStore {
@@ -80,6 +81,15 @@ func (s *testConfigStore) UpdateOauthToken(_ context.Context, token *tables.Tabl
return nil
}
+func (s *testConfigStore) GetClientConfig(_ context.Context) (*configstore.ClientConfig, error) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ if s.clientConfig == nil {
+ return nil, nil
+ }
+ return bifrost.Ptr(*s.clientConfig), nil
+}
+
func (s *testConfigStore) GetExpiringOauthTokens(_ context.Context, before time.Time) ([]*tables.TableOauthToken, error) {
s.mu.Lock()
defer s.mu.Unlock()
@@ -159,6 +169,19 @@ func TestTestConfigStore_GetExpiringOauthTokens(t *testing.T) {
})
}
+func TestMCPTempTokenAuthEnabled(t *testing.T) {
+ store := newTestConfigStore()
+ provider := NewOAuth2Provider(store, bifrost.NewDefaultLogger(schemas.LogLevelError))
+
+ assert.False(t, provider.mcpTempTokenAuthEnabled(context.Background()))
+
+ store.clientConfig = &configstore.ClientConfig{}
+ assert.False(t, provider.mcpTempTokenAuthEnabled(context.Background()))
+
+ store.clientConfig.MCPEnableTempTokenAuth = true
+ assert.True(t, provider.mcpTempTokenAuthEnabled(context.Background()))
+}
+
func TestTokenRefreshWorker_TransientError_DoesNotMarkExpired(t *testing.T) {
// A 503 response from the token server is a transient failure.
// The oauth_config must stay "authorized" so the connection can
diff --git a/helm-charts/bifrost/templates/_helpers.tpl b/helm-charts/bifrost/templates/_helpers.tpl
index 25024917c27..0d3b7a0da1d 100644
--- a/helm-charts/bifrost/templates/_helpers.tpl
+++ b/helm-charts/bifrost/templates/_helpers.tpl
@@ -307,6 +307,9 @@ false
{{- if hasKey .Values.bifrost.client "mcpDisableAutoToolInject" }}
{{- $_ := set $client "mcp_disable_auto_tool_inject" .Values.bifrost.client.mcpDisableAutoToolInject }}
{{- end }}
+{{- if hasKey .Values.bifrost.client "mcpEnableTempTokenAuth" }}
+{{- $_ := set $client "mcp_enable_temp_token_auth" .Values.bifrost.client.mcpEnableTempTokenAuth }}
+{{- end }}
{{- if .Values.bifrost.client.routingChainMaxDepth }}
{{- $_ := set $client "routing_chain_max_depth" .Values.bifrost.client.routingChainMaxDepth }}
{{- end }}
@@ -1666,4 +1669,4 @@ Call this template at the beginning of deployment/stateful templates
{{- end }}
{{- end }}
-{{- end -}}
\ No newline at end of file
+{{- end -}}
diff --git a/helm-charts/bifrost/values.schema.json b/helm-charts/bifrost/values.schema.json
index d0c763477c7..f3f759812b1 100644
--- a/helm-charts/bifrost/values.schema.json
+++ b/helm-charts/bifrost/values.schema.json
@@ -446,6 +446,11 @@
"description": "DEPRECATED: When true, MCP tools are not automatically injected into requests. Use bifrost.mcp.toolManagerConfig.disableAutoToolInject.",
"default": false
},
+ "mcpEnableTempTokenAuth": {
+ "type": "boolean",
+ "description": "When true, Bifrost mints and accepts scoped temp tokens for MCP per-user OAuth auth pages.",
+ "default": false
+ },
"routingChainMaxDepth": {
"type": "integer",
"minimum": 1,
diff --git a/helm-charts/bifrost/values.yaml b/helm-charts/bifrost/values.yaml
index 98ab546fa8f..c024b34976a 100644
--- a/helm-charts/bifrost/values.yaml
+++ b/helm-charts/bifrost/values.yaml
@@ -247,6 +247,7 @@ bifrost:
# mcpCodeModeBindingLevel: "server"
# mcpToolSyncInterval: 10
# mcpDisableAutoToolInject: false
+ # mcpEnableTempTokenAuth: false
# hideDeletedVirtualKeysInFilters: false # Omit deleted virtual keys from logs/MCP filter data
# whitelistedRoutes: [] # Routes that bypass auth middleware
# routingChainMaxDepth: 10 # Maximum depth for routing rule chain evaluation
@@ -1075,4 +1076,4 @@ envFrom: []
# Init containers to run before the main application container.
# Provide a list of init containers using standard Kubernetes container spec.
-initContainers: []
\ No newline at end of file
+initContainers: []
diff --git a/transports/bifrost-http/handlers/config.go b/transports/bifrost-http/handlers/config.go
index adaf2249f77..57ab4c1f388 100644
--- a/transports/bifrost-http/handlers/config.go
+++ b/transports/bifrost-http/handlers/config.go
@@ -348,6 +348,7 @@ func (h *ConfigHandler) updateConfig(ctx *fasthttp.RequestCtx) {
if payload.ClientConfig.MCPToolSyncInterval != currentConfig.MCPToolSyncInterval {
updatedConfig.MCPToolSyncInterval = payload.ClientConfig.MCPToolSyncInterval
}
+ updatedConfig.MCPEnableTempTokenAuth = payload.ClientConfig.MCPEnableTempTokenAuth
// Reload MCP tool manager config with all current values in one call
if shouldReloadMCPToolManagerConfig && h.store.MCPConfig != nil {
diff --git a/transports/bifrost-http/handlers/middlewares.go b/transports/bifrost-http/handlers/middlewares.go
index 46b4d225400..e5712373a85 100644
--- a/transports/bifrost-http/handlers/middlewares.go
+++ b/transports/bifrost-http/handlers/middlewares.go
@@ -700,11 +700,12 @@ type AuthMiddleware struct {
authConfig atomic.Pointer[configstore.AuthConfig]
wsTicketStore *WSTicketStore
tempTokensService *temptoken.Service // optional; when nil, temp-token fallback is disabled
+ tempTokensEnabled atomic.Bool
}
// InitAuthMiddleware initializes the auth middleware. The tempTokens service
-// is optional — when nil, the temp-token fallback path is disabled and the
-// middleware behaves exactly as before.
+// is optional and still gated by client config — when nil or disabled, the
+// temp-token fallback path is skipped.
func InitAuthMiddleware(store configstore.ConfigStore, wsTicketStore *WSTicketStore, tempTokensService *temptoken.Service) (*AuthMiddleware, error) {
if store == nil {
return nil, fmt.Errorf("store is not present")
@@ -726,9 +727,11 @@ func InitAuthMiddleware(store configstore.ConfigStore, wsTicketStore *WSTicketSt
clientConfig, err := store.GetClientConfig(context.Background())
if err == nil && clientConfig != nil {
am.whitelistedRoutes.Store(&clientConfig.WhitelistedRoutes)
+ am.tempTokensEnabled.Store(clientConfig.MCPEnableTempTokenAuth)
} else {
emptyRoutes := []string{}
am.whitelistedRoutes.Store(&emptyRoutes)
+ am.tempTokensEnabled.Store(false)
}
return am, nil
@@ -743,6 +746,11 @@ func (m *AuthMiddleware) UpdateWhitelistedRoutes(routes []string) {
m.whitelistedRoutes.Store(&routes)
}
+// UpdateTempTokenAuthEnabled updates whether scoped temp-token fallback auth is accepted.
+func (m *AuthMiddleware) UpdateTempTokenAuthEnabled(enabled bool) {
+ m.tempTokensEnabled.Store(enabled)
+}
+
// tryTempTokenOrUnauthorized is the last-resort auth path: a request that
// failed every conventional credential check (no Authorization header, no
// valid cookie) is given one more chance to present an X-Bifrost-Temp-Token
@@ -756,7 +764,7 @@ func (m *AuthMiddleware) UpdateWhitelistedRoutes(routes []string) {
// their own success/failure semantics and silently rescuing a bad password
// with a temp token would be surprising.
func (m *AuthMiddleware) tryTempTokenOrUnauthorized(ctx *fasthttp.RequestCtx, next fasthttp.RequestHandler) {
- if m.tempTokensService != nil {
+ if m.tempTokensService != nil && m.tempTokensEnabled.Load() {
token := string(ctx.Request.Header.Peek("X-Bifrost-Temp-Token"))
if token != "" {
validated, err := m.tempTokensService.Validate(ctx, token, string(ctx.Method()), string(ctx.Path()))
diff --git a/transports/bifrost-http/handlers/session.go b/transports/bifrost-http/handlers/session.go
index a93d6714f71..7ac3678bb09 100644
--- a/transports/bifrost-http/handlers/session.go
+++ b/transports/bifrost-http/handlers/session.go
@@ -44,6 +44,8 @@ func (h *SessionHandler) isAuthEnabled(ctx *fasthttp.RequestCtx) {
if h.configStore == nil {
SendJSON(ctx, map[string]any{
"is_auth_enabled": false,
+ "has_valid_token": false,
+ "auth_type": "none",
})
return
}
@@ -55,6 +57,8 @@ func (h *SessionHandler) isAuthEnabled(ctx *fasthttp.RequestCtx) {
if authConfig == nil {
SendJSON(ctx, map[string]any{
"is_auth_enabled": false,
+ "has_valid_token": false,
+ "auth_type": "none",
})
return
}
@@ -76,9 +80,18 @@ func (h *SessionHandler) isAuthEnabled(ctx *fasthttp.RequestCtx) {
SendJSON(ctx, map[string]any{
"is_auth_enabled": authConfig.IsEnabled,
"has_valid_token": hasValidToken,
+ "auth_type": dashboardAuthType(authConfig.IsEnabled),
})
}
+// dashboardAuthType reports the dashboard session auth mode for frontend flows.
+func dashboardAuthType(isEnabled bool) string {
+ if isEnabled {
+ return "password"
+ }
+ return "none"
+}
+
// login handles POST /api/session/login - Login a user
func (h *SessionHandler) login(ctx *fasthttp.RequestCtx) {
if h.configStore == nil {
@@ -209,7 +222,7 @@ func (h *SessionHandler) issueWSTicket(ctx *fasthttp.RequestCtx) {
SendError(ctx, fasthttp.StatusServiceUnavailable, "WebSocket tickets are not available")
return
}
- sessionToken,ok := ctx.UserValue(schemas.BifrostContextKeySessionToken).(string)
+ sessionToken, ok := ctx.UserValue(schemas.BifrostContextKeySessionToken).(string)
if !ok {
SendError(ctx, fasthttp.StatusUnauthorized, "Unauthorized")
return
diff --git a/transports/bifrost-http/lib/config.go b/transports/bifrost-http/lib/config.go
index a5eaf5572ea..9744eb1f386 100644
--- a/transports/bifrost-http/lib/config.go
+++ b/transports/bifrost-http/lib/config.go
@@ -398,6 +398,7 @@ var DefaultClientConfig = configstore.ClientConfig{
MCPAgentDepth: 10,
MCPToolExecutionTimeout: 30,
MCPCodeModeBindingLevel: string(schemas.CodeModeBindingLevelServer),
+ MCPEnableTempTokenAuth: false,
HideDeletedVirtualKeysInFilters: false,
RoutingChainMaxDepth: governance.DefaultRoutingChainMaxDepth,
}
diff --git a/transports/bifrost-http/lib/config_test.go b/transports/bifrost-http/lib/config_test.go
index ee8c1f58590..7fb095d442d 100644
--- a/transports/bifrost-http/lib/config_test.go
+++ b/transports/bifrost-http/lib/config_test.go
@@ -17271,6 +17271,7 @@ func assertDefaultClientConfigValues(t *testing.T, cc configstore.ClientConfig)
require.Equal(t, 100, cc.MaxRequestBodySizeMB, "MaxRequestBodySizeMB should default to 100")
require.Equal(t, 10, cc.MCPAgentDepth, "MCPAgentDepth should default to 10")
require.Equal(t, 30, cc.MCPToolExecutionTimeout, "MCPToolExecutionTimeout should default to 30")
+ require.Equal(t, false, cc.MCPEnableTempTokenAuth, "MCPEnableTempTokenAuth should default to false")
require.Equal(t, false, cc.Compat.ConvertTextToChat, "Compat.ConvertTextToChat should default to false")
require.Equal(t, false, cc.Compat.ConvertChatToResponses, "Compat.ConvertChatToResponses should default to false")
require.Equal(t, false, cc.Compat.ShouldDropParams, "Compat.ShouldDropParams should default to false")
diff --git a/transports/bifrost-http/server/server.go b/transports/bifrost-http/server/server.go
index fe81ea98696..89a94b13c65 100644
--- a/transports/bifrost-http/server/server.go
+++ b/transports/bifrost-http/server/server.go
@@ -135,8 +135,8 @@ type BifrostHTTPServer struct {
devPprofHandler *handlers.DevPprofHandler
IntegrationHandler *handlers.IntegrationHandler
- AuthMiddleware *handlers.AuthMiddleware
- TracingMiddleware *handlers.TracingMiddleware
+ AuthMiddleware *handlers.AuthMiddleware
+ TracingMiddleware *handlers.TracingMiddleware
WSTicketStore *handlers.WSTicketStore
TempTokens *temptoken.Service
TempTokenSweepWorker *temptoken.SweepWorker
@@ -747,6 +747,7 @@ func (s *BifrostHTTPServer) ReloadClientConfigFromConfigStore(ctx context.Contex
// Reloading whitelisted routes from the client config
if s.AuthMiddleware != nil {
s.AuthMiddleware.UpdateWhitelistedRoutes(config.WhitelistedRoutes)
+ s.AuthMiddleware.UpdateTempTokenAuthEnabled(config.MCPEnableTempTokenAuth)
}
// Reloading config in bifrost client
if s.Client != nil {
diff --git a/transports/config.schema.json b/transports/config.schema.json
index 801324d3870..33e731bb49f 100644
--- a/transports/config.schema.json
+++ b/transports/config.schema.json
@@ -218,6 +218,11 @@
"description": "DEPRECATED: When true, MCP tools are not automatically injected into requests. Use mcp.tool_manager_config.disable_auto_tool_inject.",
"default": false
},
+ "mcp_enable_temp_token_auth": {
+ "type": "boolean",
+ "description": "When true, Bifrost mints and accepts scoped temp tokens for MCP per-user OAuth auth pages.",
+ "default": false
+ },
"routing_chain_max_depth": {
"type": "integer",
"minimum": 1,
diff --git a/transports/schema_test/config_schema_test.go b/transports/schema_test/config_schema_test.go
index 4235625ba65..ea866e8b3ed 100644
--- a/transports/schema_test/config_schema_test.go
+++ b/transports/schema_test/config_schema_test.go
@@ -362,6 +362,7 @@ func TestSchemaClientMCPFields(t *testing.T) {
"mcp_code_mode_binding_level",
"mcp_tool_sync_interval",
"mcp_disable_auto_tool_inject",
+ "mcp_enable_temp_token_auth",
}
for _, field := range fields {
t.Run("client has "+field, func(t *testing.T) {
@@ -381,7 +382,8 @@ func TestSchemaClientMCPFields(t *testing.T) {
"mcp_tool_execution_timeout": 60,
"mcp_code_mode_binding_level": "server",
"mcp_tool_sync_interval": 10,
- "mcp_disable_auto_tool_inject": false
+ "mcp_disable_auto_tool_inject": false,
+ "mcp_enable_temp_token_auth": true
}
}`
if err := validateConfig(t, compiled, config); err != nil {
diff --git a/ui/app/login/layout.tsx b/ui/app/login/layout.tsx
index 80fd281b09d..4317bed4713 100644
--- a/ui/app/login/layout.tsx
+++ b/ui/app/login/layout.tsx
@@ -1,61 +1,75 @@
import { ThemeProvider } from "@/components/themeProvider";
import { ReduxProvider } from "@/lib/store/provider";
+import {
+ DEFAULT_POST_LOGIN_PATH,
+ getLoginGotoFromSearch,
+} from "@/lib/utils/loginGoto";
import { getApiBaseUrl } from "@/lib/utils/port";
import { createFileRoute, redirect } from "@tanstack/react-router";
import { NuqsAdapter } from "nuqs/adapters/tanstack-router";
import LoginPage from "./page";
function RouteComponent() {
- return (
- Configure MCP (Model Context Protocol) agent and tool settings. Maximum depth for MCP agent execution. Maximum time in seconds for tool execution.
+ Configure MCP (Model Context Protocol) agent and tool settings.
+
+ Maximum depth for MCP agent execution.
+ How often to refresh tool lists from MCP servers. Set to 0 to disable.
+ Maximum time in seconds for tool execution.
+
- When enabled, MCP tools are not automatically included in every request. Tools are only injected when explicitly specified via
- request headers (
+ How often to refresh tool lists from MCP servers. Set to 0 to
+ disable.
+
- How tools are exposed in the VFS: server-level (all tools per server) or tool-level (individual tools).
-
+ When enabled, MCP tools are not automatically included in every
+ request. Tools are only injected when explicitly specified via
+ request headers (
+ VFS Structure:
+ When enabled, per-user MCP OAuth links can include a short-lived
+ scoped token so someone without an active Bifrost dashboard
+ session can complete the flow. Keep disabled to require normal
+ dashboard authentication.
+ All tools per server in a single .py file Individual .py file for each tool
+ How tools are exposed in the VFS: server-level (all tools per
+ server) or tool-level (individual tools).
+
+ VFS Structure:
+
+ All tools per server in a single .py file
+
+ Individual .py file for each tool
+
- Override Bifrost's public base URL when it runs behind a reverse proxy.{" "}
- Leave blank to derive the URL from the incoming
- Upstream OAuth providers lock the
+ Override Bifrost's public base URL when it runs behind a reverse
+ proxy. Leave blank to derive the URL from the incoming{" "}
+
+ Upstream OAuth providers lock the{" "}
+
-
+ MCP Settings
- MCP Settings
+ x-bf-mcp-include-tools) and still must be allowed by the virtual key MCP
- configuration.
- x-bf-mcp-include-tools) and still
+ must be allowed by the virtual key MCP configuration.
+ Host header. Used as the{" "}
- redirect_uri Bifrost registers with upstream OAuth providers when it acts as a client to an
- MCP server (e.g. Notion or Jira redirect the browser to{" "}
- {" after login). Supports env var syntax (e.g.{" "}
- env.BIFROST_EXTERNAL_URL).
- redirect_uri to whatever was registered initially,
- so MCP clients that already completed OAuth will fail with "Invalid redirect URI". To recover,
- clear the stored OAuth client credentials for the affected MCP servers and re-authorize so Bifrost re-runs Dynamic
- Client Registration with the new URL.
- Host header. Used as the{" "}
+ redirect_uri Bifrost registers
+ with upstream OAuth providers when it acts as a client to an MCP
+ server (e.g. Notion or Jira redirect the browser to{" "}
+ {"{" "}
+ after login). Supports env var syntax (e.g.{" "}
+ env.BIFROST_EXTERNAL_URL).
+ redirect_uri to whatever
+ was registered initially, so MCP clients that already
+ completed OAuth will fail with{" "}
+ "Invalid redirect URI". To recover, clear
+ the stored OAuth client credentials for the affected MCP
+ servers and re-authorize so Bifrost re-runs Dynamic Client
+ Registration with the new URL.
+
+ If you continue with the temporary link, activity from this + credential will be attributed to the bound key instead of your + user account. +
+ +