Files
gitea-tea/internal/auth/credentials.go
bircni 88b91faa54 refactor: add credential and Git packages (#1123)
- `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>
2026-10-11 17:09:37 +00:00

153 lines
4.7 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package auth
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"time"
"gitea.dev/tea/internal/settings"
"github.com/zalando/go-keyring"
)
// keyringService namespaces credentials in the platform credential manager.
const keyringService = "tea-cli-v2"
// Credential holds the secret token material kept outside instance metadata.
type Credential struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token,omitempty"`
ExpiresAt time.Time `json:"expires_at,omitzero"`
}
// Keyring is the operating-system credential storage boundary.
type Keyring interface {
Get(string, string) (string, error)
Set(string, string, string) error
Delete(string, string) error
}
// SystemKeyring delegates to the platform credential manager.
type SystemKeyring struct{}
// Get retrieves a secret from the platform credential manager.
func (SystemKeyring) Get(service, user string) (string, error) { return keyring.Get(service, user) }
// Set persists a secret in the platform credential manager.
func (SystemKeyring) Set(service, user, value string) error { return keyring.Set(service, user, value) }
// Delete removes only the selected instance/account credential.
func (SystemKeyring) Delete(service, user string) error { return keyring.Delete(service, user) }
// errNoKeyring rejects the keyring backend on a Store without a Keyring.
var errNoKeyring = errors.New("keyring storage unavailable")
// Store selects secure storage or explicitly opted-in private files.
type Store struct {
Dir string
Keyring Keyring
}
// Identity builds a stable, opaque key from an instance URL and account.
func Identity(instance, user string) string {
sum := sha256.Sum256([]byte(instance + "\x00" + user))
return hex.EncodeToString(sum[:])
}
func (s *Store) filename(instance, user string) string {
return filepath.Join(s.Dir, "credentials", Identity(instance, user)+".json")
}
// Load returns a credential without exposing storage contents in errors.
func (s *Store) Load(instance, user, backend string) (Credential, error) {
var data []byte
var err error
switch backend {
case settings.StoreKeyring:
if s.Keyring == nil {
return Credential{}, errNoKeyring
}
value, loadErr := s.Keyring.Get(keyringService, Identity(instance, user))
data, err = []byte(value), loadErr
case settings.StoreFile:
data, err = os.ReadFile(s.filename(instance, user))
default:
err = errors.New("unsupported credential store")
}
if errors.Is(err, keyring.ErrNotFound) || errors.Is(err, os.ErrNotExist) {
return Credential{}, errors.New("credential unavailable; run tea auth login for this instance and account")
}
if err != nil {
return Credential{}, fmt.Errorf("credential storage unavailable: %w", err)
}
var credential Credential
if err := json.Unmarshal(data, &credential); err != nil {
return Credential{}, errors.New("invalid stored credential; run tea auth login")
}
if credential.AccessToken == "" {
return Credential{}, errors.New("stored credential has no access token")
}
return credential, nil
}
// Save persists credentials without falling back to a different backend.
func (s *Store) Save(instance, user, backend string, credential Credential) error {
if credential.AccessToken == "" {
return errors.New("access token cannot be empty")
}
data, err := json.Marshal(credential)
if err != nil {
return err
}
switch backend {
case settings.StoreKeyring:
if s.Keyring == nil {
return errNoKeyring
}
if err := s.Keyring.Set(keyringService, Identity(instance, user), string(data)); err != nil {
return errors.New("secure credential storage unavailable; use --credential-store file only if you accept private file storage")
}
return nil
case settings.StoreFile:
if err := os.MkdirAll(filepath.Dir(s.filename(instance, user)), 0o700); err != nil {
return err
}
return settings.AtomicWrite(s.filename(instance, user), data)
default:
return errors.New("unsupported credential store")
}
}
// Delete removes only the selected instance/account credential.
func (s *Store) Delete(instance, user, backend string) error {
switch backend {
case settings.StoreKeyring:
if s.Keyring == nil {
return errNoKeyring
}
err := s.Keyring.Delete(keyringService, Identity(instance, user))
if errors.Is(err, keyring.ErrNotFound) {
return nil
}
if err != nil {
return errors.New("failed to remove credential from secure storage")
}
return nil
case settings.StoreFile:
err := os.Remove(s.filename(instance, user))
if errors.Is(err, os.ErrNotExist) {
return nil
}
return err
default:
return fmt.Errorf("unsupported credential store %q", backend)
}
}