diff --git a/callback.go b/callback.go index 162bad6..a887bd6 100644 --- a/callback.go +++ b/callback.go @@ -23,9 +23,10 @@ var ErrCallbackTimeout = errors.New("browser authorization timed out") // callbackResult holds the outcome of the local callback round-trip. type callbackResult struct { - Storage *TokenStorage - Error string - Desc string + Storage *TokenStorage + Error string + Desc string // Detailed description (for terminal only) + SanitizedMsg string // User-friendly message (for browser only) } // startCallbackServer starts a local HTTP server on the given port and waits @@ -50,36 +51,48 @@ func startCallbackServer(ctx context.Context, port int, expectedState string, if oauthErr := q.Get("error"); oauthErr != "" { desc := q.Get("error_description") - writeCallbackPage(w, false, oauthErr, desc) - sendResult(callbackResult{Error: oauthErr, Desc: desc}) + sanitized := sanitizeOAuthError(oauthErr, desc) + writeCallbackPage(w, false, sanitized) + sendResult(callbackResult{Error: oauthErr, Desc: desc, SanitizedMsg: sanitized}) return } state := q.Get("state") if state != expectedState { - writeCallbackPage(w, false, "state_mismatch", - "State parameter does not match. Possible CSRF attack.") + sanitized := "Authorization failed. Possible security issue detected." + writeCallbackPage(w, false, sanitized) sendResult(callbackResult{ - Error: "state_mismatch", - Desc: "state parameter mismatch", + Error: "state_mismatch", + Desc: "State parameter does not match. Possible CSRF attack.", + SanitizedMsg: sanitized, }) return } code := q.Get("code") if code == "" { - writeCallbackPage(w, false, "missing_code", "No authorization code in callback.") - sendResult(callbackResult{Error: "missing_code", Desc: "code parameter missing"}) + sanitized := "Authorization failed. Missing authorization code." + writeCallbackPage(w, false, sanitized) + sendResult(callbackResult{ + Error: "missing_code", + Desc: "code parameter missing", + SanitizedMsg: sanitized, + }) return } storage, exchangeErr := exchangeFn(r.Context(), code) if exchangeErr != nil { - writeCallbackPage(w, false, "token_exchange_failed", exchangeErr.Error()) - sendResult(callbackResult{Error: "token_exchange_failed", Desc: exchangeErr.Error()}) + sanitized := sanitizeTokenExchangeError(exchangeErr) + writeCallbackPage(w, false, sanitized) + sendResult(callbackResult{ + Error: "token_exchange_failed", + Desc: exchangeErr.Error(), + SanitizedMsg: sanitized, + }) return } - writeCallbackPage(w, true, "", "") + writeCallbackPage(w, true, "") sendResult(callbackResult{Storage: storage}) }) @@ -121,7 +134,8 @@ func startCallbackServer(ctx context.Context, port int, expectedState string, } // writeCallbackPage writes a minimal HTML response to the browser tab. -func writeCallbackPage(w http.ResponseWriter, success bool, errCode, errDesc string) { +// The message parameter should be pre-sanitized for security. +func writeCallbackPage(w http.ResponseWriter, success bool, message string) { w.Header().Set("Content-Type", "text/html; charset=utf-8") if success { @@ -137,10 +151,6 @@ func writeCallbackPage(w http.ResponseWriter, success bool, errCode, errDesc str return } - msg := errCode - if errDesc != "" { - msg = errDesc - } fmt.Fprintf(w, `
%s
You can close this tab and check your terminal for details.
-