lfs_auth.go

 1package git
 2
 3import (
 4	"context"
 5	"encoding/json"
 6	"errors"
 7	"fmt"
 8	"time"
 9
10	log "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	jwt "github.com/golang-jwt/jwt/v5"
16)
17
18// LFSAuthenticate implements the 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	}
25
26	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	}
32
33	user := proto.UserFromContext(ctx)
34	if user == nil {
35		logger.Errorf("missing user")
36		return proto.ErrUserNotFound
37	}
38
39	repo := proto.RepositoryFromContext(ctx)
40	if repo == nil {
41		logger.Errorf("missing repository")
42		return proto.ErrRepoNotFound
43	}
44
45	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 err
50	}
51
52	now := time.Now()
53	expiresIn := time.Minute * 5
54	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 hour
58		NotBefore: jwt.NewNumericDate(now),
59		IssuedAt:  jwt.NewNumericDate(now),
60		Issuer:    cfg.HTTP.PublicURL,
61		Audience: []string{
62			repo.Name(),
63		},
64	}
65
66	token := jwt.NewWithClaims(jwk.SigningMethod, claims)
67	token.Header["kid"] = kp.JWK().KeyID
68	j, err := token.SignedString(kp.PrivateKey())
69	if err != nil {
70		logger.Error("failed to sign token", "err", err)
71		return err
72	}
73
74	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)
76
77	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}