diff --git a/cmd/dex/config.go b/cmd/dex/config.go index 2519f6f5..c0394e37 100644 --- a/cmd/dex/config.go +++ b/cmd/dex/config.go @@ -85,10 +85,11 @@ type password storage.Password func (p *password) UnmarshalJSON(b []byte) error { var data struct { - Email string `json:"email"` - Username string `json:"username"` - UserID string `json:"userID"` - Hash string `json:"hash"` + Email string `json:"email"` + Username string `json:"username"` + UserID string `json:"userID"` + Hash string `json:"hash"` + HashFromEnv string `json:"hashFromEnv"` } if err := json.Unmarshal(b, &data); err != nil { return err @@ -99,7 +100,11 @@ func (p *password) UnmarshalJSON(b []byte) error { UserID: data.UserID, }) if len(data.Hash) == 0 { - return fmt.Errorf("no password hash provided") + if len(data.HashFromEnv) > 0 { + data.Hash = os.Getenv(data.HashFromEnv) + } else { + return fmt.Errorf("no password hash provided") + } } // If this value is a valid bcrypt, use it. diff --git a/cmd/dex/config_test.go b/cmd/dex/config_test.go index d7875ad1..feb23144 100644 --- a/cmd/dex/config_test.go +++ b/cmd/dex/config_test.go @@ -1,9 +1,11 @@ package main import ( - "github.com/dexidp/dex/server" + "os" "testing" + "github.com/dexidp/dex/server" + "github.com/ghodss/yaml" "github.com/kylelemons/godebug/pretty" @@ -15,6 +17,8 @@ import ( var _ = yaml.YAMLToJSON +const testHashStaticPasswordEnv = "FOO_PASSWORD" + func TestValidConfiguration(t *testing.T) { configuration := Config{ Issuer: "http://127.0.0.1:5556/dex", @@ -213,3 +217,164 @@ logger: } } + +func TestUnmarshalConfigWithEnv(t *testing.T) { + staticPasswordEnv := os.Getenv(testHashStaticPasswordEnv) + if staticPasswordEnv == "" { + t.Skipf("test environment variable %q not set, skipping", testHashStaticPasswordEnv) + } + rawConfig := []byte(` +issuer: http://127.0.0.1:5556/dex +storage: + type: postgres + config: + host: 10.0.0.1 + port: 65432 + maxOpenConns: 5 + maxIdleConns: 3 + connMaxLifetime: 30 + connectionTimeout: 3 +web: + http: 127.0.0.1:5556 + +frontend: + dir: ./web + extra: + foo: bar + +staticClients: +- id: example-app + redirectURIs: + - 'http://127.0.0.1:5555/callback' + name: 'Example App' + secret: ZXhhbXBsZS1hcHAtc2VjcmV0 + +oauth2: + alwaysShowLoginScreen: true + +connectors: +- type: mockCallback + id: mock + name: Example +- type: oidc + id: google + name: Google + config: + issuer: https://accounts.google.com + clientID: foo + clientSecret: bar + redirectURI: http://127.0.0.1:5556/dex/callback/google + +enablePasswordDB: true +staticPasswords: +- email: "admin@example.com" + # bcrypt hash of the string "password" + hash: "$2a$10$33EMT0cVYVlPy6WAMCLsceLYjWhuHpbz5yuZxu/GAFj03J9Lytjuy" + username: "admin" + userID: "08a8684b-db88-4b73-90a9-3cd1661f5466" +- email: "foo@example.com" + hashFromEnv: "FOO_PASSWORD" + username: "foo" + userID: "41331323-6f44-45e6-b3b9-2c4b60c02be5" + +expiry: + signingKeys: "7h" + idTokens: "25h" + authRequests: "25h" + +logger: + level: "debug" + format: "json" +`) + + want := Config{ + Issuer: "http://127.0.0.1:5556/dex", + Storage: Storage{ + Type: "postgres", + Config: &sql.Postgres{ + NetworkDB: sql.NetworkDB{ + Host: "10.0.0.1", + Port: 65432, + MaxOpenConns: 5, + MaxIdleConns: 3, + ConnMaxLifetime: 30, + ConnectionTimeout: 3, + }, + }, + }, + Web: Web{ + HTTP: "127.0.0.1:5556", + }, + Frontend: server.WebConfig{ + Dir: "./web", + Extra: map[string]string{ + "foo": "bar", + }, + }, + StaticClients: []storage.Client{ + { + ID: "example-app", + Secret: "ZXhhbXBsZS1hcHAtc2VjcmV0", + Name: "Example App", + RedirectURIs: []string{ + "http://127.0.0.1:5555/callback", + }, + }, + }, + OAuth2: OAuth2{ + AlwaysShowLoginScreen: true, + }, + StaticConnectors: []Connector{ + { + Type: "mockCallback", + ID: "mock", + Name: "Example", + Config: &mock.CallbackConfig{}, + }, + { + Type: "oidc", + ID: "google", + Name: "Google", + Config: &oidc.Config{ + Issuer: "https://accounts.google.com", + ClientID: "foo", + ClientSecret: "bar", + RedirectURI: "http://127.0.0.1:5556/dex/callback/google", + }, + }, + }, + EnablePasswordDB: true, + StaticPasswords: []password{ + { + Email: "admin@example.com", + Hash: []byte("$2a$10$33EMT0cVYVlPy6WAMCLsceLYjWhuHpbz5yuZxu/GAFj03J9Lytjuy"), + Username: "admin", + UserID: "08a8684b-db88-4b73-90a9-3cd1661f5466", + }, + { + Email: "foo@example.com", + Hash: []byte("$2a$10$33EMT0cVYVlPy6WAMCLsceLYjWhuHpbz5yuZxu/GAFj03J9Lytjuy"), + Username: "foo", + UserID: "41331323-6f44-45e6-b3b9-2c4b60c02be5", + }, + }, + Expiry: Expiry{ + SigningKeys: "7h", + IDTokens: "25h", + AuthRequests: "25h", + }, + Logger: Logger{ + Level: "debug", + Format: "json", + }, + } + + var c Config + if err := yaml.Unmarshal(rawConfig, &c); err != nil { + t.Fatalf("failed to decode config: %v", err) + } + if diff := pretty.Compare(c, want); diff != "" { + t.Errorf("got!=want: %s", diff) + } + +} diff --git a/storage/storage.go b/storage/storage.go index cb2a7e0c..42ecd8ed 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -292,6 +292,9 @@ type Password struct { // Bcrypt encoded hash of the password. This package enforces a min cost value of 10 Hash []byte `json:"hash"` + // Bcrypt encoded hash of the password set in environment variable of this name. + HashFromEnv string `json:"hashFromEnv"` + // Optional username to display. NOT used during login. Username string `json:"username"`