1package git
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "time"
9
10 "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)
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}