mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 11:01:25 +01:00
- `internal/auth`: credentials in the OS keyring or a private file, OAuth login with token refresh, SSH HTTP-signature signers (key files with passphrase prompt, ssh-agent keys and certificates), and the Git credential helper protocol. - `internal/git`: cancellable Git subprocesses, repository and remote discovery (tracked remote via `branch.<name>.remote`), PR checkout by full ref, AGit push, branch cleanup. Nothing imports these packages yet. Reviewed-on: https://gitea.com/gitea/tea/pulls/1123 Reviewed-by: TheFox0x7 <95654+thefox0x7@noreply.gitea.com>
139 lines
5.3 KiB
Go
139 lines
5.3 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package auth
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.dev/tea/internal/settings"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestOAuthLoginPKCEAndState(t *testing.T) {
|
|
var redirect, challenge string
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
require.Equal(t, "/base/login/oauth/access_token", r.URL.Path)
|
|
require.NoError(t, r.ParseForm())
|
|
require.Equal(t, "authorization_code", r.Form.Get("grant_type"))
|
|
require.Equal(t, "valid-code", r.Form.Get("code"))
|
|
require.NotEmpty(t, r.Form.Get("code_verifier"))
|
|
require.Equal(t, redirect, r.Form.Get("redirect_uri"))
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"access_token": "access", "refresh_token": "refresh", "expires_in": 3600, "token_type": "Bearer"})
|
|
}))
|
|
defer server.Close()
|
|
credential, err := OAuthLogin(context.Background(), OAuthOptions{Instance: server.URL + "/base", ClientID: "configured-client", HTTP: server.Client(), Timeout: time.Second, Authorize: func(ctx context.Context, raw string) error {
|
|
u, err := url.Parse(raw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
require.Equal(t, "configured-client", u.Query().Get("client_id"))
|
|
require.Equal(t, "S256", u.Query().Get("code_challenge_method"))
|
|
redirect = u.Query().Get("redirect_uri")
|
|
callback, err := url.Parse(redirect)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "127.0.0.1", callback.Hostname())
|
|
// Gitea's built-in tea application registers only the bare loopback URL.
|
|
require.Empty(t, callback.Path)
|
|
challenge = u.Query().Get("code_challenge")
|
|
require.NotEmpty(t, challenge)
|
|
// An unrelated callback cannot consume or satisfy the authorization flow.
|
|
response, err := http.Get(redirect + "?code=invalid&state=wrong")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
response.Body.Close()
|
|
require.Equal(t, 400, response.StatusCode)
|
|
response, err = http.Get(redirect + "?code=valid-code&state=" + url.QueryEscape(u.Query().Get("state")))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
response.Body.Close()
|
|
return nil
|
|
}})
|
|
require.NoError(t, err)
|
|
require.Equal(t, "access", credential.AccessToken)
|
|
require.Equal(t, "refresh", credential.RefreshToken)
|
|
}
|
|
|
|
func TestRefreshSerializesAndRetainsRefreshToken(t *testing.T) {
|
|
config := settings.New(t.TempDir())
|
|
keyring := &memoryKeyring{values: map[string]string{}}
|
|
credentials := &Store{Dir: config.Dir, Keyring: keyring}
|
|
manager := newTestManager(t, config, credentials)
|
|
requests := 0
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
requests++
|
|
require.NoError(t, r.ParseForm())
|
|
require.Equal(t, "old-refresh", r.Form.Get("refresh_token"))
|
|
require.Equal(t, "client", r.Form.Get("client_id"))
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = fmt.Fprintln(w, `{"access_token":"new-access","expires_in":3600,"token_type":"Bearer"}`)
|
|
}))
|
|
defer server.Close()
|
|
now := time.Now()
|
|
require.NoError(t, manager.Register(server.URL, "alice", "file", "oauth", HostOptions{OAuthClientID: "client"}, Credential{AccessToken: "old-access", RefreshToken: "old-refresh", ExpiresAt: now.Add(-time.Minute)}))
|
|
var wg sync.WaitGroup
|
|
results := make(chan Credential, 8)
|
|
failures := make(chan error, 8)
|
|
for range 8 {
|
|
wg.Go(func() {
|
|
c, err := manager.Refresh(context.Background(), server.URL, "alice", server.Client(), false, now)
|
|
results <- c
|
|
failures <- err
|
|
})
|
|
}
|
|
wg.Wait()
|
|
close(results)
|
|
close(failures)
|
|
for err := range failures {
|
|
require.NoError(t, err)
|
|
}
|
|
for credential := range results {
|
|
require.Equal(t, "new-access", credential.AccessToken)
|
|
require.Equal(t, "old-refresh", credential.RefreshToken)
|
|
}
|
|
require.Equal(t, 1, requests)
|
|
}
|
|
|
|
func TestOAuthWithoutHTTPClientUsesDefaultClient(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = fmt.Fprintln(w, `{"access_token":"access","refresh_token":"refresh","expires_in":3600,"token_type":"Bearer"}`)
|
|
}))
|
|
defer server.Close()
|
|
credential, err := OAuthLogin(context.Background(), OAuthOptions{Instance: server.URL, Timeout: time.Second, Authorize: func(_ context.Context, raw string) error {
|
|
u, err := url.Parse(raw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
response, err := http.Get(u.Query().Get("redirect_uri") + "?code=code&state=" + url.QueryEscape(u.Query().Get("state")))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return response.Body.Close()
|
|
}})
|
|
require.NoError(t, err)
|
|
require.Equal(t, "access", credential.AccessToken)
|
|
config := settings.New(t.TempDir())
|
|
manager := newTestManager(t, config, &Store{Dir: config.Dir})
|
|
require.NoError(t, manager.Register(server.URL, "alice", "file", "oauth", HostOptions{}, Credential{AccessToken: "old", RefreshToken: "refresh", ExpiresAt: time.Now().Add(-time.Minute)}))
|
|
credential, err = manager.Refresh(context.Background(), server.URL, "alice", nil, false, time.Now())
|
|
require.NoError(t, err)
|
|
require.Equal(t, "access", credential.AccessToken)
|
|
}
|
|
|
|
func TestRefreshEndsBeforeSettingsLockWaitersGiveUp(t *testing.T) {
|
|
// A refresh holds the settings lock, so concurrent tea processes must wait longer than it can take.
|
|
require.Less(t, refreshTimeout, settings.LockTimeout)
|
|
}
|