giteabot backport / giteabot (push) Canceled after 0s
giteabot / giteabot (push) Canceled after 0s
release-nightly / nightly-binary (push) Canceled after 0s
release-nightly / nightly-container (push) Canceled after 0s
release-nightly-snapcraft / build-and-publish (push) Canceled after 0s
Generate a per-login verifier, persist it in the Goth session, send an S256 challenge, and include the verifier during the token exchange when explicitly enabled. Assisted-by: Codex:GPT-5
127 lines
4.3 KiB
Go
127 lines
4.3 KiB
Go
// 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)
|
|
}
|