Skip to content

Commit 09c4f02

Browse files
feat: add refresh token expiry reauth flow for per user mcp oauth
1 parent 76e9d94 commit 09c4f02

12 files changed

Lines changed: 978 additions & 35 deletions

File tree

core/mcp/clientmanager.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func (m *MCPManager) ReconnectClient(id string) error {
5757
// Reconnect is not applicable because auth is resolved per request/user identity.
5858
if client.ExecutionConfig != nil && client.ExecutionConfig.AuthType == schemas.MCPAuthTypePerUserOauth {
5959
m.mu.Unlock()
60-
return fmt.Errorf("reconnect is not supported for per_user_oauth clients")
60+
return fmt.Errorf("per-user OAuth clients do not maintain a shared upstream connection (each user manages their own auth): %w", schemas.ErrMCPReconnectNotApplicable)
6161
}
6262
config := client.ExecutionConfig
6363
m.mu.Unlock()

core/mcp/utils/utils.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ func ResolvePerUserOAuthToken(ctx *schemas.BifrostContext, client *schemas.MCPCl
2525
}
2626

2727
accessToken, err := oauth2Provider.GetUserAccessTokenByIdentity(ctx, virtualKeyID, userID, sessionToken, client.ExecutionConfig.ID)
28-
if err != nil && !errors.Is(err, schemas.ErrOAuth2TokenNotFound) {
28+
// Both sentinels mean "this user must re-authenticate":
29+
// - ErrOAuth2TokenNotFound: row missing (never authed, or purged after permanent refresh failure)
30+
// - ErrOAuth2TokenExpired: row present but tokens unusable (access expired + no refresh available)
31+
// Either way, fall through to the re-auth branch below to surface an inline auth URL.
32+
if err != nil && !errors.Is(err, schemas.ErrOAuth2TokenNotFound) && !errors.Is(err, schemas.ErrOAuth2TokenExpired) {
2933
return "", fmt.Errorf("failed to get user access token for MCP server %s: %w", client.ExecutionConfig.Name, err)
3034
}
3135
if err != nil {

core/schemas/mcp.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ var (
2929
ErrOAuth2NotPerUserSession = errors.New("state does not match a per-user oauth session")
3030
ErrOAuth2TokenNotFound = errors.New("per-user oauth token not found for this identity and mcp server")
3131
ErrPerUserOAuthPendingFlowExpired = errors.New("per-user oauth pending flow has expired")
32+
// ErrMCPReconnectNotApplicable signals that the reconnect operation is not
33+
// meaningful for this client type — e.g. per-user OAuth clients, where
34+
// each user manages their own auth and there is no shared upstream
35+
// connection to "reconnect". Distinct from "not implemented".
36+
ErrMCPReconnectNotApplicable = errors.New("reconnect is not applicable for this client type")
3237
)
3338

3439
// MCPUserOAuthRequiredError is returned when a per-user OAuth MCP server requires

docs/mcp/gateway-url.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,10 @@ The `WWW-Authenticate` URL above and the `redirect_uri` Bifrost hands to upstrea
312312

313313
In most setups both are the same public URL. They can differ when the management/discovery surface and the OAuth callback surface live behind different proxies. See [Reverse Proxy configuration →](../deployment-guides/config-json/client#reverse-proxy) for the full reference and examples.
314314

315+
<Warning>
316+
**Changing `mcp_external_client_url` breaks already-connected MCP clients.** Upstream OAuth providers lock the `redirect_uri` to whatever was registered during Dynamic Client Registration (RFC 7591). If you change this URL afterwards, existing clients fail with **"Invalid redirect URI"** at the authorize step. To recover, clear the stored OAuth client credentials for the affected MCP server and re-authorize so Bifrost re-registers with the new URL.
317+
</Warning>
318+
315319
---
316320

317321
## Security Considerations

docs/mcp/oauth.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,10 @@ Bifrost will:
189189
2. Register a new client using `registration_url`
190190
3. Use the registered client ID for authorization
191191

192+
<Warning>
193+
The `redirect_uri` is registered with the upstream provider at this point and is **locked** to Bifrost's current client URL (`mcp_external_client_url`, or the `Host` header if unset). If you later change Bifrost's public URL, the upstream provider will reject the authorize call with **"Invalid redirect URI"**. To recover, clear the stored client credentials for this MCP server so Bifrost re-runs Dynamic Client Registration with the new URL.
194+
</Warning>
195+
192196
#### OAuth Discovery
193197

194198
Bifrost can automatically discover OAuth endpoints from your MCP server's metadata:
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module oauth-demo-server
2+
3+
go 1.26.2
4+
5+
require github.com/mark3labs/mcp-go v0.43.2
6+
7+
require (
8+
github.com/bahlo/generic-list-go v0.2.0 // indirect
9+
github.com/buger/jsonparser v1.1.1 // indirect
10+
github.com/google/uuid v1.6.0 // indirect
11+
github.com/invopop/jsonschema v0.13.0 // indirect
12+
github.com/mailru/easyjson v0.7.7 // indirect
13+
github.com/spf13/cast v1.7.1 // indirect
14+
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
15+
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
16+
gopkg.in/yaml.v3 v3.0.1 // indirect
17+
)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
2+
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
3+
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
4+
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
5+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
6+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7+
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
8+
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
9+
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
10+
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
11+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
12+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
13+
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
14+
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
15+
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
16+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
17+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
18+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
19+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
20+
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
21+
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
22+
github.com/mark3labs/mcp-go v0.43.2 h1:21PUSlWWiSbUPQwXIJ5WKlETixpFpq+WBpbMGDSVy/I=
23+
github.com/mark3labs/mcp-go v0.43.2/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
24+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
25+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
26+
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
27+
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
28+
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
29+
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
30+
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
31+
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
32+
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
33+
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
34+
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
35+
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
36+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
37+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
38+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
39+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 commit comments

Comments
 (0)