Implement JWT token auth with transparent auto-renewal.
Replace per-call SSH signing with a two-layer auth system: Server: AuthInterceptor verifies JWT tokens (HMAC-SHA256 signed with repo-local jwt.key). Authenticate RPC accepts SSH-signed challenges and issues 30-day JWTs. Expired-but-valid tokens return a ReauthChallenge in error details (server-provided nonce for fast re-auth). Authenticate RPC is exempt from token requirement. Client: TokenCredentials replaces SSHCredentials as the primary PerRPCCredentials. NewWithAuth creates clients with auto-renewal — EnsureAuth obtains initial token, retryOnAuth catches Unauthenticated errors and re-authenticates transparently. Token cached at $XDG_STATE_HOME/sgard/token (0600). CLI: dialRemote() helper handles token loading, connection setup, and initial auth. Push/pull/prune commands simplified to use it. Proto: Added Authenticate RPC, AuthenticateRequest/Response, ReauthChallenge messages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/kisom/sgard/client"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -47,6 +51,41 @@ func resolveRemote() (string, error) {
|
||||
return "", fmt.Errorf("no remote configured; use --remote, SGARD_REMOTE, or create %s/remote", repoFlag)
|
||||
}
|
||||
|
||||
// dialRemote creates a gRPC client with token-based auth and auto-renewal.
|
||||
func dialRemote(ctx context.Context) (*client.Client, func(), error) {
|
||||
addr, err := resolveRemote()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
signer, err := client.LoadSigner(sshKeyFlag)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
cachedToken := client.LoadCachedToken()
|
||||
creds := client.NewTokenCredentials(cachedToken)
|
||||
|
||||
conn, err := grpc.NewClient(addr,
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithPerRPCCredentials(creds),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("connecting to %s: %w", addr, err)
|
||||
}
|
||||
|
||||
c := client.NewWithAuth(conn, creds, signer)
|
||||
|
||||
// Ensure we have a valid token before proceeding.
|
||||
if err := c.EnsureAuth(ctx); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, nil, fmt.Errorf("authentication: %w", err)
|
||||
}
|
||||
|
||||
cleanup := func() { _ = conn.Close() }
|
||||
return c, cleanup, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
rootCmd.PersistentFlags().StringVar(&repoFlag, "repo", defaultRepo(), "path to sgard repository")
|
||||
rootCmd.PersistentFlags().StringVar(&remoteFlag, "remote", "", "gRPC server address (host:port)")
|
||||
|
||||
@@ -4,11 +4,8 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/kisom/sgard/client"
|
||||
"github.com/kisom/sgard/garden"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
var pruneCmd = &cobra.Command{
|
||||
@@ -19,7 +16,7 @@ var pruneCmd = &cobra.Command{
|
||||
addr, _ := resolveRemote()
|
||||
|
||||
if addr != "" {
|
||||
return pruneRemote(addr)
|
||||
return pruneRemote()
|
||||
}
|
||||
return pruneLocal()
|
||||
},
|
||||
@@ -40,24 +37,16 @@ func pruneLocal() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func pruneRemote(addr string) error {
|
||||
signer, err := client.LoadSigner(sshKeyFlag)
|
||||
func pruneRemote() error {
|
||||
ctx := context.Background()
|
||||
|
||||
c, cleanup, err := dialRemote(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
creds := client.NewSSHCredentials(signer)
|
||||
conn, err := grpc.NewClient(addr,
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithPerRPCCredentials(creds),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting to %s: %w", addr, err)
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
c := client.New(conn)
|
||||
removed, err := c.Prune(context.Background())
|
||||
removed, err := c.Prune(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -4,44 +4,28 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/kisom/sgard/client"
|
||||
"github.com/kisom/sgard/garden"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
var pullCmd = &cobra.Command{
|
||||
Use: "pull",
|
||||
Short: "Pull checkpoint from remote server",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
addr, err := resolveRemote()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
g, err := garden.Open(repoFlag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
signer, err := client.LoadSigner(sshKeyFlag)
|
||||
c, cleanup, err := dialRemote(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
creds := client.NewSSHCredentials(signer)
|
||||
conn, err := grpc.NewClient(addr,
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithPerRPCCredentials(creds),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting to %s: %w", addr, err)
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
c := client.New(conn)
|
||||
pulled, err := c.Pull(context.Background(), g)
|
||||
pulled, err := c.Pull(ctx, g)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -8,41 +8,26 @@ import (
|
||||
"github.com/kisom/sgard/client"
|
||||
"github.com/kisom/sgard/garden"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
var pushCmd = &cobra.Command{
|
||||
Use: "push",
|
||||
Short: "Push local checkpoint to remote server",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
addr, err := resolveRemote()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
g, err := garden.Open(repoFlag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
signer, err := client.LoadSigner(sshKeyFlag)
|
||||
c, cleanup, err := dialRemote(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
creds := client.NewSSHCredentials(signer)
|
||||
conn, err := grpc.NewClient(addr,
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithPerRPCCredentials(creds),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting to %s: %w", addr, err)
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
c := client.New(conn)
|
||||
pushed, err := c.Push(context.Background(), g)
|
||||
pushed, err := c.Push(ctx, g)
|
||||
if errors.Is(err, client.ErrServerNewer) {
|
||||
fmt.Println("Server is newer; run sgard pull instead.")
|
||||
return nil
|
||||
|
||||
@@ -29,9 +29,10 @@ var rootCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
var opts []grpc.ServerOption
|
||||
var srvInstance *server.Server
|
||||
|
||||
if authKeysPath != "" {
|
||||
auth, err := server.NewAuthInterceptor(authKeysPath)
|
||||
auth, err := server.NewAuthInterceptor(authKeysPath, repoPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading authorized keys: %w", err)
|
||||
}
|
||||
@@ -39,13 +40,15 @@ var rootCmd = &cobra.Command{
|
||||
grpc.UnaryInterceptor(auth.UnaryInterceptor()),
|
||||
grpc.StreamInterceptor(auth.StreamInterceptor()),
|
||||
)
|
||||
srvInstance = server.NewWithAuth(g, auth)
|
||||
fmt.Printf("Auth enabled: %s\n", authKeysPath)
|
||||
} else {
|
||||
srvInstance = server.New(g)
|
||||
fmt.Println("WARNING: no --authorized-keys specified, running without authentication")
|
||||
}
|
||||
|
||||
srv := grpc.NewServer(opts...)
|
||||
sgardpb.RegisterGardenSyncServer(srv, server.New(g))
|
||||
sgardpb.RegisterGardenSyncServer(srv, srvInstance)
|
||||
|
||||
lis, err := net.Listen("tcp", listenAddr)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user