Skip to content

Commit 55bc660

Browse files
fix: handle elicitation cancellation in OAuth flows
1 parent 5076265 commit 55bc660

File tree

3 files changed

+62
-31
lines changed

3 files changed

+62
-31
lines changed

internal/oauth/manager.go

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -114,25 +114,37 @@ func (m *Manager) startDeviceFlowWithElicitation(ctx context.Context, session *m
114114
return fmt.Errorf("failed to get device authorization: %w", err)
115115
}
116116

117+
// Create cancellable context for polling
118+
pollCtx, cancelPoll := context.WithCancel(ctx)
119+
defer cancelPoll()
120+
117121
// Use session elicitation if available to show the user the verification URL and code
118122
if session != nil {
119-
elicitID, err := generateRandomToken()
120-
if err != nil {
121-
// Log warning but continue - elicitation ID is for tracking only
122-
elicitID = "fallback-id"
123-
}
124-
// Elicitation result is not critical - device flow polls independently
125-
_, _ = session.Elicit(ctx, &mcp.ElicitParams{
126-
Mode: "url",
127-
URL: deviceAuth.VerificationURI,
128-
ElicitationID: elicitID,
129-
Message: fmt.Sprintf("GitHub OAuth Device Authorization\n\nYour code: %s\n\nVisit the URL and enter this code to authenticate.", deviceAuth.UserCode),
130-
})
123+
// Run elicitation in goroutine - if cancelled, abort the device flow
124+
go func() {
125+
elicitID, err := generateRandomToken()
126+
if err != nil {
127+
elicitID = "fallback-id"
128+
}
129+
result, err := session.Elicit(ctx, &mcp.ElicitParams{
130+
Mode: "url",
131+
URL: deviceAuth.VerificationURI,
132+
ElicitationID: elicitID,
133+
Message: fmt.Sprintf("GitHub OAuth Device Authorization\n\nYour code: %s\n\nVisit the URL and enter this code to authenticate.", deviceAuth.UserCode),
134+
})
135+
// If elicitation was cancelled or declined, abort the polling
136+
if err != nil || result == nil || result.Action == "cancel" || result.Action == "decline" {
137+
cancelPoll()
138+
}
139+
}()
131140
}
132141

133-
// Poll for the token (blocking)
134-
token, err := oauth2Cfg.DeviceAccessToken(ctx, deviceAuth)
142+
// Poll for the token (blocking, but respects context cancellation)
143+
token, err := oauth2Cfg.DeviceAccessToken(pollCtx, deviceAuth)
135144
if err != nil {
145+
if pollCtx.Err() != nil {
146+
return fmt.Errorf("OAuth authorization was cancelled by user")
147+
}
136148
return fmt.Errorf("failed to get device access token: %w", err)
137149
}
138150

@@ -203,16 +215,29 @@ func (m *Manager) startPKCEFlowWithElicitation(ctx context.Context, session *mcp
203215
// Try to open browser - if it works, no elicitation needed
204216
browserErr := openBrowser(authURL)
205217

218+
// Channel to signal elicitation cancellation
219+
elicitCancelChan := make(chan struct{}, 1)
220+
206221
// Only elicit if browser failed to open (e.g., headless environment)
207222
// and we need to show the user the URL manually
208223
if browserErr != nil && session != nil {
209-
elicitID, _ := generateRandomToken()
210-
_, _ = session.Elicit(ctx, &mcp.ElicitParams{
211-
Mode: "url",
212-
URL: authURL,
213-
ElicitationID: elicitID,
214-
Message: "GitHub OAuth Authorization\n\nPlease visit the URL to authorize access.",
215-
})
224+
// Run elicitation in goroutine so we can monitor callback in parallel
225+
go func() {
226+
elicitID, _ := generateRandomToken()
227+
result, err := session.Elicit(ctx, &mcp.ElicitParams{
228+
Mode: "url",
229+
URL: authURL,
230+
ElicitationID: elicitID,
231+
Message: "GitHub OAuth Authorization\n\nPlease visit the URL to authorize access.",
232+
})
233+
// If elicitation was cancelled or declined, signal to abort
234+
if err != nil || result == nil || result.Action == "cancel" || result.Action == "decline" {
235+
select {
236+
case elicitCancelChan <- struct{}{}:
237+
default:
238+
}
239+
}
240+
}()
216241
}
217242

218243
// Wait for callback with timeout
@@ -239,6 +264,10 @@ func (m *Manager) startPKCEFlowWithElicitation(ctx context.Context, session *mcp
239264
cleanup()
240265
return fmt.Errorf("OAuth callback error: %w", err)
241266

267+
case <-elicitCancelChan:
268+
cleanup()
269+
return fmt.Errorf("OAuth authorization was cancelled by user")
270+
242271
case <-ctx.Done():
243272
cleanup()
244273
return ctx.Err()

internal/oauth/templates/error.html

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66
<title>Authorization Failed</title>
77
<link href="https://cdn.jsdelivr.net/npm/@primer/css@22/dist/primer.min.css" rel="stylesheet">
88
<style>
9-
html, body { height: 100%; }
10-
body { display: flex; align-items: center; justify-content: center; }
9+
html, body { height: 100%; margin: 0; }
10+
body { display: flex; align-items: center; justify-content: center; min-height: 100vh; }
11+
.card { width: 500px; box-shadow: none !important; }
1112
</style>
1213
</head>
13-
<body class="color-bg-default">
14-
<div class="Box color-shadow-medium p-6 text-center" style="max-width: 480px;">
15-
<svg class="octicon color-fg-default mb-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="64" height="64" fill="currentColor">
14+
<body class="color-bg-canvas">
15+
<div class="Box color-bg-default card p-6 text-center">
16+
<svg class="octicon color-fg-default mb-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="80" height="80" fill="currentColor">
1617
<path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"></path>
1718
</svg>
1819
<h1 class="h2 color-fg-danger mb-3">Authorization Failed</h1>

internal/oauth/templates/success.html

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66
<title>Authorization Successful</title>
77
<link href="https://cdn.jsdelivr.net/npm/@primer/css@22/dist/primer.min.css" rel="stylesheet">
88
<style>
9-
html, body { height: 100%; }
10-
body { display: flex; align-items: center; justify-content: center; }
9+
html, body { height: 100%; margin: 0; }
10+
body { display: flex; align-items: center; justify-content: center; min-height: 100vh; }
11+
.card { width: 500px; box-shadow: none !important; }
1112
</style>
1213
</head>
13-
<body class="color-bg-default">
14-
<div class="Box color-shadow-medium p-6 text-center" style="max-width: 480px;">
15-
<svg class="octicon color-fg-default mb-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="64" height="64" fill="currentColor">
14+
<body class="color-bg-canvas">
15+
<div class="Box color-bg-default card p-6 text-center">
16+
<svg class="octicon color-fg-default mb-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="80" height="80" fill="currentColor">
1617
<path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"></path>
1718
</svg>
1819
<h1 class="h2 color-fg-success mb-3">Authorization Successful</h1>

0 commit comments

Comments
 (0)