package idp
import (
"bytes"
"encoding/json"
"net/http"
"net/url"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func TestGeminiRegistersAllRelayCallbacks(t *testing.T) {
hosts := []string{
"oauth-redirect.googleusercontent.com",
"oauth-redirect-sandbox.googleusercontent.com",
"oauth-redirect-test.googleusercontent.com",
}
var prefixes, redirects []string
for _, host := range hosts {
prefixes = append(prefixes, "https://"+host+"/")
for _, callback := range []string{"/r/user_bound_custom-mcp-test", "/other-callback/custom-mcp-test"} {
redirects = append(redirects, "https://"+host+callback)
}
}
policy := strings.Join(prefixes, ",")
var oldPrefixes []string
for _, prefix := range prefixes {
oldPrefixes = append(oldPrefixes, prefix+"r/")
}
require.False(t, validRedirectURIs(redirects, strings.Join(oldPrefixes, ",")))
require.False(t, validRedirectURIs(redirects, prefixes[0]))
require.True(t, validRedirectURIs(redirects, policy))
t.Setenv("MCP_REDIRECT_PREFIXES", policy)
server, _, _ := setupTestServer(t)
defer server.Close()
register := func(t *testing.T, uris []string) *http.Response {
t.Helper()
body, err := json.Marshal(registrationRequest{
RedirectURIs: uris, TokenEndpointAuthMethod: "client_secret_basic",
GrantTypes: []string{"authorization_code", "refresh_token"},
ResponseTypes: []string{"code"},
})
require.NoError(t, err)
response, err := http.Post(server.URL+RegistrationEndpoint, "application/json", bytes.NewReader(body))
require.NoError(t, err)
return response
}
response := register(t, redirects)
defer response.Body.Close()
require.Equal(t, http.StatusCreated, response.StatusCode)
var client registrationResponse
require.NoError(t, json.NewDecoder(response.Body).Decode(&client))
require.Equal(t, redirects, client.RedirectURIs)
require.NotEmpty(t, client.ClientID)
require.NotEmpty(t, client.ClientSecret)
require.Equal(t, "client_secret_basic", client.TokenEndpointAuthMethod)
t.Run("authorize requires an exact registered callback", func(t *testing.T) {
browser := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse
}}
query := url.Values{
"client_id": {client.ClientID}, "response_type": {"code"},
"redirect_uri": {"https://" + hosts[0] + "/other-callback/unregistered"},
"state": {"redirect-mismatch-test"}, "code_challenge": {strings.Repeat("a", 43)},
"code_challenge_method": {"S256"},
}
response, err := browser.Get(server.URL + AuthorizationEndpoint + "?" + query.Encode())
require.NoError(t, err)
defer response.Body.Close()
require.Equal(t, http.StatusBadRequest, response.StatusCode)
require.Empty(t, response.Header.Get("Location"))
var rejection map[string]any
require.NoError(t, json.NewDecoder(response.Body).Decode(&rejection))
require.Contains(t, rejection["error_description"], "redirect_uri")
})
for _, host := range hosts {
for _, bad := range []string{
"https://" + host + ".evil/r/steal",
"http://" + host + "/r/steal",
"https://" + host + ":444/r/steal",
"https://" + strings.Replace(host, "oauth-redirect", "unknown-relay", 1) + "/r/steal",
"https://attacker@" + host + "/r/steal",
"https://" + host + "/r/steal?value=1",
"https://" + host + "/r/steal#fragment",
"https://" + host + "/other/%2e%2e/steal",
"https://" + host + "/r/../steal",
} {
t.Run(bad, func(t *testing.T) {
mixed := append([]string(nil), redirects...)
mixed[len(mixed)-1] = bad
response := register(t, mixed)
defer response.Body.Close()
require.Equal(t, http.StatusBadRequest, response.StatusCode)
})
}
}
}