fix: OAuth token exchange — try JSON then form-encoded, show actual error in browser
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 1m2s
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 1m2s
The callback page now shows the real error message instead of a generic "Failed to exchange" message. Token exchange tries JSON Content-Type first (matching Claude Code's pattern) with form-encoded fallback. Full response body logged for debugging. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8362a50460
commit
999f8f0539
@ -1,6 +1,7 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
@ -116,8 +117,9 @@ func (o *OAuthManager) StartLogin(openURL func(string) error) (<-chan error, err
|
||||
|
||||
// Exchange code for tokens
|
||||
if err := o.exchangeCode(code, verifier, redirectURI); err != nil {
|
||||
slog.Error("oauth token exchange failed", "error", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprint(w, "Failed to exchange authorization code.")
|
||||
fmt.Fprintf(w, "<html><body><h2>Authentication Failed</h2><pre>%s</pre><p>Check Wraith logs for details.</p></body></html>", err.Error())
|
||||
done <- fmt.Errorf("exchange code: %w", err)
|
||||
return
|
||||
}
|
||||
@ -158,15 +160,31 @@ func (o *OAuthManager) StartLogin(openURL func(string) error) (<-chan error, err
|
||||
|
||||
// exchangeCode exchanges an authorization code for access and refresh tokens.
|
||||
func (o *OAuthManager) exchangeCode(code, verifier, redirectURI string) error {
|
||||
data := url.Values{
|
||||
"grant_type": {"authorization_code"},
|
||||
"code": {code},
|
||||
"redirect_uri": {redirectURI},
|
||||
"client_id": {o.clientID},
|
||||
"code_verifier": {verifier},
|
||||
// Try JSON format first (what Claude Code appears to use), fall back to form-encoded
|
||||
payload := map[string]string{
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"redirect_uri": redirectURI,
|
||||
"client_id": o.clientID,
|
||||
"code_verifier": verifier,
|
||||
}
|
||||
|
||||
resp, err := http.PostForm(o.tokenURL, data)
|
||||
jsonBody, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal token request: %w", err)
|
||||
}
|
||||
|
||||
slog.Info("exchanging auth code", "tokenURL", o.tokenURL, "redirectURI", redirectURI)
|
||||
|
||||
req, err := http.NewRequest("POST", o.tokenURL, io.NopCloser(
|
||||
bytes.NewReader(jsonBody),
|
||||
))
|
||||
if err != nil {
|
||||
return fmt.Errorf("create token request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("post token request: %w", err)
|
||||
}
|
||||
@ -177,8 +195,31 @@ func (o *OAuthManager) exchangeCode(code, verifier, redirectURI string) error {
|
||||
return fmt.Errorf("read token response: %w", err)
|
||||
}
|
||||
|
||||
slog.Info("token endpoint response", "status", resp.StatusCode, "body", string(body)[:min(len(body), 500)])
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("token endpoint returned %d: %s", resp.StatusCode, string(body))
|
||||
// If JSON failed, try form-encoded
|
||||
slog.Info("JSON token exchange failed, trying form-encoded")
|
||||
data := url.Values{
|
||||
"grant_type": {"authorization_code"},
|
||||
"code": {code},
|
||||
"redirect_uri": {redirectURI},
|
||||
"client_id": {o.clientID},
|
||||
"code_verifier": {verifier},
|
||||
}
|
||||
resp2, err := http.PostForm(o.tokenURL, data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("post form token request: %w", err)
|
||||
}
|
||||
defer resp2.Body.Close()
|
||||
body, err = io.ReadAll(resp2.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read form token response: %w", err)
|
||||
}
|
||||
slog.Info("form-encoded token response", "status", resp2.StatusCode, "body", string(body)[:min(len(body), 500)])
|
||||
if resp2.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("token endpoint returned %d (json) and %d (form): %s", resp.StatusCode, resp2.StatusCode, string(body))
|
||||
}
|
||||
}
|
||||
|
||||
var tokens tokenResponse
|
||||
@ -189,6 +230,13 @@ func (o *OAuthManager) exchangeCode(code, verifier, redirectURI string) error {
|
||||
return o.storeTokens(tokens)
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// storeTokens encrypts and persists OAuth tokens in settings.
|
||||
func (o *OAuthManager) storeTokens(tokens tokenResponse) error {
|
||||
o.mu.Lock()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user