// Copyright 2026 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package oauth2 import ( "encoding/base64" "encoding/json" "fmt" "net/http" "net/http/httptest" "net/url" "testing" "time" "gitea.dev/modules/setting" "gitea.dev/modules/test" "github.com/markbates/goth/providers/openidConnect" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" go_oauth2 "golang.org/x/oauth2" ) func TestOpenIDConnectPKCEProvider(t *testing.T) { var codeChallenge string var codeVerifier string var serverURL string server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { switch request.URL.Path { case "/.well-known/openid-configuration": response.Header().Set("Content-Type", "application/json") require.NoError(t, json.NewEncoder(response).Encode(map[string]string{ "issuer": serverURL, "authorization_endpoint": serverURL + "/authorize", "token_endpoint": serverURL + "/token", })) case "/token": require.NoError(t, request.ParseForm()) codeVerifier = request.Form.Get("code_verifier") if codeVerifier == "" || go_oauth2.S256ChallengeFromVerifier(codeVerifier) != codeChallenge { http.Error(response, "invalid PKCE verifier", http.StatusBadRequest) return } claims, err := json.Marshal(map[string]any{ "aud": "client-id", "exp": time.Now().Add(time.Hour).Unix(), "iss": serverURL, "sub": "person-1", }) require.NoError(t, err) response.Header().Set("Content-Type", "application/json") require.NoError(t, json.NewEncoder(response).Encode(map[string]any{ "access_token": "access-token", "expires_in": 3600, "id_token": "e30." + base64.RawURLEncoding.EncodeToString(claims) + ".signature", "token_type": "Bearer", })) default: http.NotFound(response, request) } })) defer server.Close() serverURL = server.URL defer test.MockVariableValue(&setting.OAuth2Client.EnableOpenIDConnectPKCE, true)() createdProvider, err := (&OpenIDProvider{}).CreateGothProvider( "example", "https://git.example.com/user/oauth2/example/callback", &Source{ ClientID: "client-id", ClientSecret: "client-secret", OpenIDConnectAutoDiscoveryURL: server.URL + "/.well-known/openid-configuration", Scopes: []string{"openid"}, }, ) require.NoError(t, err) pkceProvider, ok := createdProvider.(*openIDConnectPKCEProvider) require.True(t, ok) pkceProvider.Provider.SkipUserInfoRequest = true pkceProvider.SetName("example") session, err := pkceProvider.BeginAuth("test-state") require.NoError(t, err) authURL, err := session.GetAuthURL() require.NoError(t, err) parsedAuthURL, err := url.Parse(authURL) require.NoError(t, err) assert.Equal(t, "S256", parsedAuthURL.Query().Get("code_challenge_method")) codeChallenge = parsedAuthURL.Query().Get("code_challenge") assert.NotEmpty(t, codeChallenge) restoredSession, err := pkceProvider.UnmarshalSession(session.Marshal()) require.NoError(t, err) accessToken, err := restoredSession.Authorize(pkceProvider, url.Values{"code": {"authorization-code"}}) require.NoError(t, err) assert.Equal(t, "access-token", accessToken) assert.NotEmpty(t, codeVerifier) user, err := pkceProvider.FetchUser(restoredSession) require.NoError(t, err) assert.Equal(t, "person-1", user.UserID) assert.Equal(t, "example", user.Provider) } func TestOpenIDConnectPKCESessionRejectsMissingVerifier(t *testing.T) { provider := &openIDConnectPKCEProvider{Provider: &openidConnect.Provider{}} session := &openIDConnectPKCESession{} _, err := session.Authorize(provider, url.Values{"code": {"authorization-code"}}) assert.EqualError(t, err, "OpenID Connect PKCE session is missing the code verifier") _, err = provider.UnmarshalSession(fmt.Sprintf(`{"session":{"AuthURL":%q}}`, "https://id.example.com/authorize")) assert.EqualError(t, err, "OpenID Connect PKCE session is missing the code verifier") } func TestAsOpenIDConnectProvider(t *testing.T) { provider := &openidConnect.Provider{} actual, ok := asOpenIDConnectProvider(provider) require.True(t, ok) assert.Same(t, provider, actual) actual, ok = asOpenIDConnectProvider(newOpenIDConnectPKCEProvider(provider)) require.True(t, ok) assert.Same(t, provider, actual) }