mirror of
https://gitea.com/gitea/tea.git
synced 2026-06-25 22:27:39 +02:00
fix(oauth): pass resolved redirect_uri to token exchange (#1019)
When --redirect-url is omitted, the local callback listener binds to a free port and opts.RedirectURL is rewritten with it. oauth2Config.RedirectURL was never updated, so Exchange() sent the stale http://127.0.0.1:0 while the authorize step had sent the real port. RFC-6749-compliant servers (Gitea >= #37704, current Forgejo) reject the mismatch. Propagate the resolved URL back into oauth2Config before Exchange. Add a regression test using httptest that drives the flow end-to-end and asserts the redirect_uri values match. --------- Co-authored-by: dbankmann <204984+dbankmann@users.noreply.gitea.com> Reviewed-on: https://gitea.com/gitea/tea/pulls/1019 Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: Daniel Bankmann <204984+dbankmann@noreply.gitea.com> Co-committed-by: Daniel Bankmann <204984+dbankmann@noreply.gitea.com>
This commit is contained in:
committed by
Lunny Xiao
parent
4b209d68de
commit
b11d991d1e
@@ -0,0 +1,91 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Regression test for the redirect_uri propagation bug: with --redirect-url
|
||||
// omitted, the callback listener picks a free port and opts.RedirectURL is
|
||||
// rewritten in place. The OAuth2 config must see that rewritten URL so the
|
||||
// token exchange sends the same redirect_uri the authorize step advertised
|
||||
// (RFC 6749 §4.1.3).
|
||||
func TestPerformBrowserOAuthFlow_RedirectURIMatchesAcrossAuthorizeAndExchange(t *testing.T) {
|
||||
var (
|
||||
mu sync.Mutex
|
||||
authorizeRedirectURI string
|
||||
exchangeRedirectURI string
|
||||
)
|
||||
|
||||
idp := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/login/oauth/authorize":
|
||||
mu.Lock()
|
||||
authorizeRedirectURI = r.URL.Query().Get("redirect_uri")
|
||||
mu.Unlock()
|
||||
cb, err := url.Parse(r.URL.Query().Get("redirect_uri"))
|
||||
require.NoError(t, err)
|
||||
q := cb.Query()
|
||||
q.Set("code", "test-code")
|
||||
q.Set("state", r.URL.Query().Get("state"))
|
||||
cb.RawQuery = q.Encode()
|
||||
http.Redirect(w, r, cb.String(), http.StatusFound)
|
||||
|
||||
case "/login/oauth/access_token":
|
||||
require.NoError(t, r.ParseForm())
|
||||
mu.Lock()
|
||||
exchangeRedirectURI = r.PostForm.Get("redirect_uri")
|
||||
mu.Unlock()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"access_token": "test-token",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
})
|
||||
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
t.Cleanup(idp.Close)
|
||||
|
||||
// Replace the real browser opener with a goroutine that fetches the
|
||||
// authorize URL, just like a real browser would.
|
||||
origOpenBrowser := openBrowser
|
||||
t.Cleanup(func() { openBrowser = origOpenBrowser })
|
||||
openBrowser = func(authURL string) error {
|
||||
go func() {
|
||||
resp, err := http.Get(authURL)
|
||||
if err == nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Leave RedirectURL empty so tea picks a random port.
|
||||
_, token, err := performBrowserOAuthFlow(context.Background(), OAuthOptions{
|
||||
URL: idp.URL,
|
||||
ClientID: "test-client-id",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "test-token", token.AccessToken)
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
require.NotEmpty(t, authorizeRedirectURI)
|
||||
require.NotEmpty(t, exchangeRedirectURI)
|
||||
assert.Equal(t, authorizeRedirectURI, exchangeRedirectURI,
|
||||
"redirect_uri must match between authorize and token exchange (RFC 6749 §4.1.3)")
|
||||
}
|
||||
Reference in New Issue
Block a user