loginsrv

Unnamed repository; edit this file 'description' to name the repository.
git clone git@jamesshield.xyz:repos/loginsrv.git
Log | Files | Refs | README | LICENSE

commit be2ae2ce04f3f9e5287e78121636134cc49b2fa2
parent 84ee0ad74502592f1cad19518306b71c9f2c9295
Author: Jakob Rockenbauch <34475067+J-Rocke@users.noreply.github.com>
Date:   Mon, 18 Nov 2019 10:33:44 +0100

Merge pull request #143 from kernle32dll/implement-secret-file

Implement option to provide jwt secret via file
Diffstat:
MREADME.md | 75++++++++++++++++++++++++++++++++++++++-------------------------------------
Mcaddy/README.md | 2++
Mcaddy/setup.go | 5+++++
Mcaddy/setup_test.go | 1+
Mlogin/config.go | 22++++++++++++++++++++++
Mlogin/config_test.go | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 147 insertions(+), 37 deletions(-)

diff --git a/README.md b/README.md @@ -56,43 +56,44 @@ For questions and support please use the [Gitter chat room](https://gitter.im/ta _Note for Caddy users_: Not all parameters are available in Caddy. See the table for details. With Caddy, the parameter names can also be used with `_` in the names, e.g. `cookie_http_only`. -| Parameter | Type | Default | Caddy | Description | -|-----------------------------|-------------|--------------|-------|--------------------------------------------------------------------------------------------| -| -cookie-domain | string | | X | Optional domain parameter for the cookie | -| -cookie-expiry | string | session | X | Expiry duration for the cookie, e.g. 2h or 3h30m | -| -cookie-http-only | boolean | true | X | Set the cookie with the HTTP only flag | -| -cookie-name | string | "jwt_token" | X | Name of the JWT cookie | -| -cookie-secure | boolean | true | X | Set the secure flag on the JWT cookie. (Set this to false for plain HTTP support) | -| -github | value | | X | OAuth config in the form: client_id=..,client_secret=..[,scope=..][,redirect_uri=..] | -| -google | value | | X | OAuth config in the form: client_id=..,client_secret=..[,scope=..][,redirect_uri=..] | -| -bitbucket | value | | X | OAuth config in the form: client_id=..,client_secret=..[,scope=..][,redirect_uri=..] | -| -facebook | value | | X | OAuth config in the form: client_id=..,client_secret=..[,scope=..][,redirect_uri=..] | -| -gitlab | value | | X | OAuth config in the form: client_id=..,client_secret=..[,scope=..,][redirect_uri=..] | -| -host | string | "localhost" | - | Host to listen on | -| -htpasswd | value | | X | Htpasswd login backend opts: file=/path/to/pwdfile | -| -jwt-expiry | go duration | 24h | X | Expiry duration for the JWT token, e.g. 2h or 3h30m | -| -jwt-secret | string | "random key" | X | Secret used to sign the JWT token. (See [caddy/README.md](./caddy/README.md) for details.) | -| -jwt-algo | string | "HS512" | X | Signing algorithm to use (ES256, ES384, ES512, HS512, HS256, HS384, HS512) | -| -log-level | string | "info" | - | Log level | -| -login-path | string | "/login" | X | Path of the login resource | -| -logout-url | string | | X | URL or path to redirect to after logout | -| -osiam | value | | X | OSIAM login backend opts: endpoint=..,client_id=..,client_secret=.. | -| -port | string | "6789" | - | Port to listen on | -| -redirect | boolean | true | X | Allow dynamic overwriting of the the success by query parameter | -| -redirect-query-parameter | string | "backTo" | X | URL parameter for the redirect target | -| -redirect-check-referer | boolean | true | X | Check the referer header to ensure it matches the host header on dynamic redirects | -| -redirect-host-file | string | "" | X | A file containing a list of domains that redirects are allowed to, one domain per line | -| -simple | value | | X | Simple login backend opts: user1=password,user2=password,.. | -| -success-url | string | "/" | X | URL to redirect to after login | -| -prevent-external-redirects | boolean | true | X | Prevent dynamic redirects to external domains | -| -template | string | | X | An alternative template for the login form | -| -text-logging | boolean | true | - | Log in text format instead of JSON | -| -jwt-refreshes | int | 0 | X | The maximum number of JWT refreshes | -| -grace-period | go duration | 5s | - | Duration to wait after SIGINT/SIGTERM for existing requests. No new requests are accepted. | -| -user-file | string | | X | A YAML file with user specific data for the tokens. (see below for an example) | -| -user-endpoint | string | | X | URL of an endpoint providing user specific data for the tokens. (see below for an example) | -| -user-endpoint-token | string | | X | Authentication token used when communicating with the user endpoint | -| -user-endpoint-timeout | go duration | 5s | X | Timeout used when communicating with the user endpoint | +| Parameter | Type | Default | Caddy | Description | +|-----------------------------|-------------|--------------|-------|-------------------------------------------------------------------------------------------------------| +| -cookie-domain | string | | X | Optional domain parameter for the cookie | +| -cookie-expiry | string | session | X | Expiry duration for the cookie, e.g. 2h or 3h30m | +| -cookie-http-only | boolean | true | X | Set the cookie with the HTTP only flag | +| -cookie-name | string | "jwt_token" | X | Name of the JWT cookie | +| -cookie-secure | boolean | true | X | Set the secure flag on the JWT cookie. (Set this to false for plain HTTP support) | +| -github | value | | X | OAuth config in the form: client_id=..,client_secret=..[,scope=..][,redirect_uri=..] | +| -google | value | | X | OAuth config in the form: client_id=..,client_secret=..[,scope=..][,redirect_uri=..] | +| -bitbucket | value | | X | OAuth config in the form: client_id=..,client_secret=..[,scope=..][,redirect_uri=..] | +| -facebook | value | | X | OAuth config in the form: client_id=..,client_secret=..[,scope=..][,redirect_uri=..] | +| -gitlab | value | | X | OAuth config in the form: client_id=..,client_secret=..[,scope=..,][redirect_uri=..] | +| -host | string | "localhost" | - | Host to listen on | +| -htpasswd | value | | X | Htpasswd login backend opts: file=/path/to/pwdfile | +| -jwt-expiry | go duration | 24h | X | Expiry duration for the JWT token, e.g. 2h or 3h30m | +| -jwt-secret | string | "random key" | X | Secret used to sign the JWT token. (See [caddy/README.md](./caddy/README.md) for details.) | +| -jwt-secret-file | string | | X | File to load the jwt-secret from, e.g. `/run/secrets/some.key`. **Takes precedence over jwt-secret!** | +| -jwt-algo | string | "HS512" | X | Signing algorithm to use (ES256, ES384, ES512, RS256, RS384, RS512, HS256, HS384, HS512) | +| -log-level | string | "info" | - | Log level | +| -login-path | string | "/login" | X | Path of the login resource | +| -logout-url | string | | X | URL or path to redirect to after logout | +| -osiam | value | | X | OSIAM login backend opts: endpoint=..,client_id=..,client_secret=.. | +| -port | string | "6789" | - | Port to listen on | +| -redirect | boolean | true | X | Allow dynamic overwriting of the the success by query parameter | +| -redirect-query-parameter | string | "backTo" | X | URL parameter for the redirect target | +| -redirect-check-referer | boolean | true | X | Check the referer header to ensure it matches the host header on dynamic redirects | +| -redirect-host-file | string | "" | X | A file containing a list of domains that redirects are allowed to, one domain per line | +| -simple | value | | X | Simple login backend opts: user1=password,user2=password,.. | +| -success-url | string | "/" | X | URL to redirect to after login | +| -prevent-external-redirects | boolean | true | X | Prevent dynamic redirects to external domains | +| -template | string | | X | An alternative template for the login form | +| -text-logging | boolean | true | - | Log in text format instead of JSON | +| -jwt-refreshes | int | 0 | X | The maximum number of JWT refreshes | +| -grace-period | go duration | 5s | - | Duration to wait after SIGINT/SIGTERM for existing requests. No new requests are accepted. | +| -user-file | string | | X | A YAML file with user specific data for the tokens. (see below for an example) | +| -user-endpoint | string | | X | URL of an endpoint providing user specific data for the tokens. (see below for an example) | +| -user-endpoint-token | string | | X | Authentication token used when communicating with the user endpoint | +| -user-endpoint-timeout | go duration | 5s | X | Timeout used when communicating with the user endpoint | ### Environment Variables All of the above Config Options can also be applied as environment variables by using variables named this way: `LOGINSRV_OPTION_NAME`. diff --git a/caddy/README.md b/caddy/README.md @@ -14,6 +14,8 @@ If a secret was configured in the directive config, this has higher priority and that both are set. This way, it is also possible to configure different secrets for multiple hosts. If no secret was set at all, a random token is generated and used. +**Note:** If using `JWT_SECRET_FILE` (see root README), `JWT_SECRET` is filled with the secret, to maintain compatibility. + To be compatible with caddy-jwt the secret is also written to the environment variable JWT_SECRET, if this variable was not set before. This enables caddy-jwt to look up the same shared secret, even in the case of a random token. If the configuration uses different tokens for different server blocks, only the first one will be stored in environment variable. You can't use a random key as the jwt-secret diff --git a/caddy/setup.go b/caddy/setup.go @@ -96,6 +96,10 @@ func parseConfig(c *caddy.Controller) (*login.Config, error) { } } + if err := cfg.ResolveFileReferences(); err != nil { + return nil, err + } + secretFromEnv, secretFromEnvWasSetBefore := os.LookupEnv("JWT_SECRET") if !secretProvidedByConfig && secretFromEnvWasSetBefore { cfg.JwtSecret = secretFromEnv @@ -105,5 +109,6 @@ func parseConfig(c *caddy.Controller) (*login.Config, error) { // but do not change a environment variable, which somebody has set it. os.Setenv("JWT_SECRET", cfg.JwtSecret) } + return cfg, nil } diff --git a/caddy/setup_test.go b/caddy/setup_test.go @@ -102,6 +102,7 @@ func TestSetup(t *testing.T) { {input: "login {\n backend \n}", shouldErr: true}, {input: "login {\n backend provider=foo\n}", shouldErr: true}, {input: "login {\n backend kk\n}", shouldErr: true}, + {input: "login {\n jwt_secret_file does-not-exist\n}", shouldErr: true}, } { t.Run(fmt.Sprintf("test %v", j), func(t *testing.T) { c := caddy.NewTestController("http", test.input) diff --git a/login/config.go b/login/config.go @@ -4,6 +4,7 @@ import ( "errors" "flag" "fmt" + "io/ioutil" "math/rand" "os" "strings" @@ -59,6 +60,7 @@ type Config struct { LogLevel string TextLogging bool JwtSecret string + JwtSecretFile string JwtAlgo string JwtExpiry time.Duration JwtRefreshes int @@ -110,6 +112,21 @@ func (c *Config) addBackendOpts(providerName, optsKvList string) error { return nil } +// ResolveFileReferences resolves configuration values, which are dynamically referenced via files +func (c *Config) ResolveFileReferences() error { + // Try to load the secret from a file, if set + if c.JwtSecretFile != "" { + secretBytes, err := ioutil.ReadFile(c.JwtSecretFile) + if err != nil { + return err + } + + c.JwtSecret = string(secretBytes) + } + + return nil +} + // ConfigureFlagSet adds all flags to the supplied flag set func (c *Config) ConfigureFlagSet(f *flag.FlagSet) { f.StringVar(&c.Host, "host", c.Host, "The host to listen on") @@ -117,6 +134,7 @@ func (c *Config) ConfigureFlagSet(f *flag.FlagSet) { f.StringVar(&c.LogLevel, "log-level", c.LogLevel, "The log level") f.BoolVar(&c.TextLogging, "text-logging", c.TextLogging, "Log in text format instead of json") f.StringVar(&c.JwtSecret, "jwt-secret", c.JwtSecret, "The secret to sign the jwt token") + f.StringVar(&c.JwtSecretFile, "jwt-secret-file", c.JwtSecretFile, "Path to a file containing the secret to sign the jwt token (overrides jwt-secret)") f.StringVar(&c.JwtAlgo, "jwt-algo", c.JwtAlgo, "The singing algorithm to use (ES256, ES384, ES512, RS256, RS384, RS512, HS256, HS384, HS512") f.DurationVar(&c.JwtExpiry, "jwt-expiry", c.JwtExpiry, "The expiry duration for the jwt token, e.g. 2h or 3h30m") f.IntVar(&c.JwtRefreshes, "jwt-refreshes", c.JwtRefreshes, "The maximum amount of jwt refreshes. 0 by Default") @@ -206,6 +224,10 @@ func readConfig(f *flag.FlagSet, args []string) (*Config, error) { return nil, err } + if err := config.ResolveFileReferences(); err != nil { + return nil, err + } + return config, err } diff --git a/login/config_test.go b/login/config_test.go @@ -2,6 +2,8 @@ package login import ( "flag" + "fmt" + "io/ioutil" "os" "testing" "time" @@ -95,6 +97,58 @@ func TestConfig_ReadConfig(t *testing.T) { Equal(t, expected, cfg) } +func TestConfig_ReadConfig_SecretFile(t *testing.T) { + // create a temporary file, containing the desired secret + testSecret := "superSecret" + + file, err := ioutil.TempFile("", "") + NoError(t, err) + defer func() { + // cleanup after test + NoError(t, os.Remove(file.Name())) + }() + + _, err = file.WriteString(testSecret) + NoError(t, err) + + // ----------- + + input := []string{ + "--jwt-secret=discardedSecret", + fmt.Sprintf("--jwt-secret-file=%s", file.Name()), + } + + cfg, err := readConfig(flag.NewFlagSet("", flag.ContinueOnError), input) + NoError(t, err) + + Equal(t, testSecret, cfg.JwtSecret) +} + +func TestConfig_ReadConfig_SecretFile_Error(t *testing.T) { + input := []string{ + "--jwt-secret=someSecret", + "--jwt-secret-file=does-not-exist", + } + + cfg, err := readConfig(flag.NewFlagSet("", flag.ContinueOnError), input) + Nil(t, cfg) + Error(t, err) + IsType(t, err, &os.PathError{}) +} + +func TestConfig_ResolveFileReferences_Error(t *testing.T) { + defaultConfig := DefaultConfig() + defaultConfig.JwtSecretFile = "does-not-exist" + + generatedKey := defaultConfig.JwtSecret + + err := defaultConfig.ResolveFileReferences() + Error(t, err) + + // existing key is not touched on file error + Equal(t, generatedKey, defaultConfig.JwtSecret) +} + func TestConfig_ReadConfigFromEnv(t *testing.T) { NoError(t, os.Setenv("LOGINSRV_HOST", "host")) NoError(t, os.Setenv("LOGINSRV_PORT", "port")) @@ -167,3 +221,28 @@ func TestConfig_ReadConfigFromEnv(t *testing.T) { NoError(t, err) Equal(t, expected, cfg) } + +func TestConfig_ReadConfigFromEnv_SecretFile(t *testing.T) { + // create a temporary file, containing the desired secret + testSecret := "superSecret" + + file, err := ioutil.TempFile("", "") + NoError(t, err) + defer func() { + // cleanup after test + NoError(t, os.Remove(file.Name())) + }() + + _, err = file.WriteString(testSecret) + NoError(t, err) + + // ----------- + + NoError(t, os.Setenv("LOGINSRV_JWT_SECRET", "discardedSecret")) + NoError(t, os.Setenv("LOGINSRV_JWT_SECRET_FILE", file.Name())) + + cfg, err := readConfig(flag.NewFlagSet("", flag.ContinueOnError), []string{}) + NoError(t, err) + + Equal(t, testSecret, cfg.JwtSecret) +}