diff --git a/README.md b/README.md index 97f148364c..2356833644 100644 --- a/README.md +++ b/README.md @@ -603,6 +603,33 @@ When specifying `auth.method = "token"` in `frpc.toml` and `frps.toml` - token b Make sure to specify the same `auth.token` in `frps.toml` and `frpc.toml` for frpc to pass frps validation +#### File-based Token Authentication + +If you prefer not to include the token directly in the configuration file, you can specify the path to a file containing the token using the `auth.tokenFile` option. + +Instead of setting `auth.token`, specify the path to the file containing your token with `auth.tokenFile`. +Ensure that the file contains only the token and no additional content. Example configuration: + +```toml +# frpc.toml +[common] +auth.tokenFile = "/path/to/your/token_file" +``` + +```toml +# frps.toml +[common] +auth.tokenFile = "/path/to/your/token_file" +``` + +Make sure the file at the specified path is readable by the application and contains the following: + +``` txt +your_secret_token +``` + +**Notes:** If both `auth.token` and `auth.tokenFile` are specified, the token from `auth.token` will be used. + #### OIDC Authentication When specifying `auth.method = "oidc"` in `frpc.toml` and `frps.toml` - OIDC based authentication will be used. diff --git a/pkg/config/flags.go b/pkg/config/flags.go index 98f617be48..bb213bca9a 100644 --- a/pkg/config/flags.go +++ b/pkg/config/flags.go @@ -165,6 +165,7 @@ func RegisterClientCommonConfigFlags(cmd *cobra.Command, c *v1.ClientCommonConfi } cmd.PersistentFlags().StringVarP(&c.User, "user", "u", "", "user") cmd.PersistentFlags().StringVarP(&c.Auth.Token, "token", "t", "", "auth token") + cmd.PersistentFlags().StringVarP(&c.Auth.TokenFile, "token_file", "", "", "auth token file") } type PortsRangeSliceFlag struct { @@ -240,6 +241,7 @@ func RegisterServerConfigFlags(cmd *cobra.Command, c *v1.ServerConfig, opts ...R cmd.PersistentFlags().Int64VarP(&c.Log.MaxDays, "log_max_days", "", 3, "log max days") cmd.PersistentFlags().BoolVarP(&c.Log.DisablePrintColor, "disable_log_color", "", false, "disable log color in console") cmd.PersistentFlags().StringVarP(&c.Auth.Token, "token", "t", "", "auth token") + cmd.PersistentFlags().StringVarP(&c.Auth.TokenFile, "token_file", "", "", "auth token file") cmd.PersistentFlags().StringVarP(&c.SubDomainHost, "subdomain_host", "", "", "subdomain host") cmd.PersistentFlags().VarP(&PortsRangeSliceFlag{V: &c.AllowPorts}, "allow_ports", "", "allow ports") cmd.PersistentFlags().Int64VarP(&c.MaxPortsPerClient, "max_ports_per_client", "", 0, "max ports per client") diff --git a/pkg/config/load.go b/pkg/config/load.go index f9a705eb21..d621ec88f7 100644 --- a/pkg/config/load.go +++ b/pkg/config/load.go @@ -103,6 +103,15 @@ func LoadFileContentWithTemplate(path string, values *Values) ([]byte, error) { return RenderWithTemplate(b, values) } +func LoadFileToken(path string) (string, error) { + data, err := os.ReadFile(path) + if err != nil { + return "", err + } + token := strings.TrimSpace(string(data)) + return token, nil +} + func LoadConfigureFromFile(path string, c any, strict bool) error { content, err := LoadFileContentWithTemplate(path, GetValues()) if err != nil { diff --git a/pkg/config/v1/client.go b/pkg/config/v1/client.go index d43ec1bcee..dc0a7abcf9 100644 --- a/pkg/config/v1/client.go +++ b/pkg/config/v1/client.go @@ -179,12 +179,20 @@ type AuthClientConfig struct { // Token specifies the authorization token used to create keys to be sent // to the server. The server must have a matching token for authorization // to succeed. By default, this value is "". - Token string `json:"token,omitempty"` - OIDC AuthOIDCClientConfig `json:"oidc,omitempty"` + Token string `json:"token,omitempty"` + TokenFile string `json:"tokenFile,omitempty"` + OIDC AuthOIDCClientConfig `json:"oidc,omitempty"` } func (c *AuthClientConfig) Complete() { c.Method = util.EmptyOr(c.Method, "token") + + if c.Token == "" && c.TokenFile != "" { + tokenFromFile, err := util.LoadFileToken(c.TokenFile) + if err == nil { + c.Token = tokenFromFile + } + } } type AuthOIDCClientConfig struct { diff --git a/pkg/config/v1/server.go b/pkg/config/v1/server.go index 3108cd34f4..32a0643d65 100644 --- a/pkg/config/v1/server.go +++ b/pkg/config/v1/server.go @@ -126,11 +126,18 @@ type AuthServerConfig struct { Method AuthMethod `json:"method,omitempty"` AdditionalScopes []AuthScope `json:"additionalScopes,omitempty"` Token string `json:"token,omitempty"` + TokenFile string `json:"tokenFile,omitempty"` OIDC AuthOIDCServerConfig `json:"oidc,omitempty"` } func (c *AuthServerConfig) Complete() { c.Method = util.EmptyOr(c.Method, "token") + if c.Token == "" && c.TokenFile != "" { + tokenFromFile, err := util.LoadFileToken(c.TokenFile) + if err == nil { + c.Token = tokenFromFile + } + } } type AuthOIDCServerConfig struct { diff --git a/pkg/util/util/util.go b/pkg/util/util/util.go index 7758054d94..c4a21a161d 100644 --- a/pkg/util/util/util.go +++ b/pkg/util/util/util.go @@ -22,6 +22,7 @@ import ( "fmt" mathrand "math/rand/v2" "net" + "os" "strconv" "strings" "time" @@ -134,3 +135,12 @@ func RandomSleep(duration time.Duration, minRatio, maxRatio float64) time.Durati func ConstantTimeEqString(a, b string) bool { return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1 } + +func LoadFileToken(path string) (string, error) { + data, err := os.ReadFile(path) + if err != nil { + return "", err + } + token := strings.TrimSpace(string(data)) + return token, nil +} diff --git a/test/e2e/v1/basic/client_server.go b/test/e2e/v1/basic/client_server.go index 1627078185..82a64dc537 100644 --- a/test/e2e/v1/basic/client_server.go +++ b/test/e2e/v1/basic/client_server.go @@ -2,6 +2,7 @@ package basic import ( "fmt" + "os" "strings" "time" @@ -149,6 +150,64 @@ var _ = ginkgo.Describe("[Feature: Client-Server]", func() { client: `auth.token = "invalid"`, expectError: true, }) + + defineClientServerTest("Client Token File Correct", f, &generalTestConfigures{ + server: `auth.token = "123456"`, + client: func() string { + tokenFile := f.WriteTempFile("correct_token.txt", "123456") + framework.AddCleanupAction(func() { + os.Remove(tokenFile) + }) + return fmt.Sprintf(`auth.tokenFile = "%s"`, tokenFile) + }(), + }) + + defineClientServerTest("Server Client Token File Correct", f, &generalTestConfigures{ + server: func() string { + tokenFile := f.WriteTempFile("correct_token.txt", "123456") + framework.AddCleanupAction(func() { + os.Remove(tokenFile) + }) + return fmt.Sprintf(`auth.tokenFile = "%s"`, tokenFile) + }(), + client: `auth.token = "123456"`, + }) + + defineClientServerTest("Client Token File Incorrect", f, &generalTestConfigures{ + server: `auth.token = "123456"`, + client: func() string { + tokenFile := f.WriteTempFile("incorrect_token.txt", "invalid") + framework.AddCleanupAction(func() { + os.Remove(tokenFile) + }) + return fmt.Sprintf(`auth.tokenFile = "%s"`, tokenFile) + }(), + expectError: true, + }) + + defineClientServerTest("Server Token File Incorrect", f, &generalTestConfigures{ + server: func() string { + tokenFile := f.WriteTempFile("incorrect_token.txt", "invalid") + framework.AddCleanupAction(func() { + os.Remove(tokenFile) + }) + return fmt.Sprintf(`auth.tokenFile = "%s"`, tokenFile) + }(), + client: `auth.token = "123456"`, + expectError: true, + }) + + defineClientServerTest("Client Token File Not Exist", f, &generalTestConfigures{ + server: `auth.token = "123456"`, + client: `auth.tokenFile = "/path/to/non/existent/file"`, + expectError: true, + }) + + defineClientServerTest("Server Token File Not Exist", f, &generalTestConfigures{ + server: `auth.tokenFile = "/path/to/non/existent/file"`, + client: `auth.token = "123456"`, + expectError: true, + }) }) ginkgo.Describe("TLS", func() {