1package git23import (4 "context"5 "encoding/json"6 "errors"7 "fmt"8 "time"910 "github.com/charmbracelet/log/v2"11 "github.com/charmbracelet/soft-serve/pkg/config"12 "github.com/charmbracelet/soft-serve/pkg/jwk"13 "github.com/charmbracelet/soft-serve/pkg/lfs"14 "github.com/charmbracelet/soft-serve/pkg/proto"15 "github.com/golang-jwt/jwt/v5"16)1718// LFSAuthenticate implements teh Git LFS SSH authentication command.19// Context must have *config.Config, *log.Logger, proto.User.20// cmd.Args should have the repo path and operation as arguments.21func LFSAuthenticate(ctx context.Context, cmd ServiceCommand) error {22 if len(cmd.Args) < 2 {23 return errors.New("missing args")24 }2526 logger := log.FromContext(ctx).WithPrefix("ssh.lfs-authenticate")27 operation := cmd.Args[1]28 if operation != lfs.OperationDownload && operation != lfs.OperationUpload {29 logger.Errorf("invalid operation: %s", operation)30 return errors.New("invalid operation")31 }3233 user := proto.UserFromContext(ctx)34 if user == nil {35 logger.Errorf("missing user")36 return proto.ErrUserNotFound37 }3839 repo := proto.RepositoryFromContext(ctx)40 if repo == nil {41 logger.Errorf("missing repository")42 return proto.ErrRepoNotFound43 }4445 cfg := config.FromContext(ctx)46 kp, err := jwk.NewPair(cfg)47 if err != nil {48 logger.Error("failed to get JWK pair", "err", err)49 return err50 }5152 now := time.Now()53 expiresIn := time.Minute * 554 expiresAt := now.Add(expiresIn)55 claims := jwt.RegisteredClaims{56 Subject: fmt.Sprintf("%s#%d", user.Username(), user.ID()),57 ExpiresAt: jwt.NewNumericDate(expiresAt), // expire in an hour58 NotBefore: jwt.NewNumericDate(now),59 IssuedAt: jwt.NewNumericDate(now),60 Issuer: cfg.HTTP.PublicURL,61 Audience: []string{62 repo.Name(),63 },64 }6566 token := jwt.NewWithClaims(jwk.SigningMethod, claims)67 token.Header["kid"] = kp.JWK().KeyID68 j, err := token.SignedString(kp.PrivateKey())69 if err != nil {70 logger.Error("failed to sign token", "err", err)71 return err72 }7374 href := fmt.Sprintf("%s/%s.git/info/lfs", cfg.HTTP.PublicURL, repo.Name())75 logger.Debug("generated token", "token", j, "href", href, "expires_at", expiresAt)7677 return json.NewEncoder(cmd.Stdout).Encode(lfs.AuthenticateResponse{78 Header: map[string]string{79 "Authorization": fmt.Sprintf("Bearer %s", j),80 },81 Href: href,82 ExpiresAt: expiresAt,83 ExpiresIn: expiresIn,84 })85}