package config import ( "crypto/rand" "encoding/hex" "errors" "fmt" "log/slog" "os" "github.com/spf13/cobra" "github.com/spf13/viper" "git.avsdev.uk/Go/job-search/internal/cli" "git.avsdev.uk/Go/job-search/internal/config" ) // csrfKeySetting is the setting generate-csrf-key stores the key in. const csrfKeySetting = "server.csrf.tokens.key" // csrfKeyBytes is the length of a generated key: 32 bytes, as the server // generates for itself when no key is set. const csrfKeyBytes = 32 // commandGenerateCSRFKey returns the "config generate-csrf-key" command, // which stores a new random CSRF key in the configuration file, or prints it. func commandGenerateCSRFKey(opts *cli.Options) *cobra.Command { cmd := &cobra.Command{ Use: "generate-csrf-key", Args: cobra.NoArgs, Short: "Generate a CSRF key and store it in the configuration file", Long: `Generate a random key for signing CSRF tokens and store it in server.csrf.tokens.key in the configuration file. Without a key, each server process signs with a random key of its own, so tokens don't survive a restart and aren't valid across instances. Give every instance the same key, and restart them to use it. The key isn't printed. With --print, it is only printed, and the configuration file isn't changed: use that to set JOBSEARCH_SERVER_CSRF_TOKENS_KEY instead, or another instance's file. An existing key is only replaced with --force, as a new key invalidates every token already issued. A key set by JOBSEARCH_SERVER_CSRF_TOKENS_KEY overrides the file, so the command refuses to store one while it is set.`, PersistentPreRun: func(cmd *cobra.Command, args []string) { slog.Debug( "command persistent pre-run", "command", "config.generate-csrf-key", ) }, PersistentPostRun: func(cmd *cobra.Command, args []string) { slog.Debug( "command persistent post-run", "command", "config.generate-csrf-key", ) }, RunE: func(cmd *cobra.Command, args []string) error { slog.Debug("command run", "command", "config.generate-csrf-key") printOnly, printErr := cmd.Flags().GetBool("print") force, forceErr := cmd.Flags().GetBool("force") err := errors.Join(printErr, forceErr) if err != nil { return err } key := generateCSRFKey() if printOnly { fmt.Fprintln(cmd.OutOrStdout(), key) //nolint:errcheck // printing only return nil } envVar := config.EnvVarName(csrfKeySetting) if os.Getenv(envVar) != "" { return fmt.Errorf( "%s is set and overrides the configuration file: "+ "unset it first, or use --print to make a key for it", envVar, ) } conf, err := opts.RunSetup(cmd, true, true) if err != nil { return fmt.Errorf("load configuration: %w", err) } if viper.ConfigFileUsed() == "" { return errors.New("no configuration file exists") } if conf.Server.CSRF.Tokens.Key != "" && !force { return fmt.Errorf( "%s is already set: --force replaces it, which invalidates "+ "every token already issued", csrfKeySetting, ) } _, _, err = config.StoreValue(conf, csrfKeySetting, key) if err != nil { return err } fmt.Fprintf( //nolint:errcheck // printing only cmd.OutOrStdout(), "%s set in %s; restart the server to use it, and give every "+ "instance the same key\n", csrfKeySetting, viper.ConfigFileUsed(), ) return nil }, } cmd.Flags().Bool( "print", false, "only print the key; don't change the configuration file", ) cmd.Flags().Bool( "force", false, "replace an existing key, invalidating issued tokens", ) cmd.MarkFlagsMutuallyExclusive("print", "force") return cmd } // generateCSRFKey returns a new random key, hex encoded as // server.csrf.tokens.key expects. func generateCSRFKey() string { key := make([]byte, csrfKeyBytes) rand.Read(key) // crypto/rand.Read never returns an error return hex.EncodeToString(key) }