Skip to content

Commit

Permalink
Merge pull request #590 from ericchiang/dev-add-password-resource
Browse files Browse the repository at this point in the history
dev branch: add a password resource for local email/password login
  • Loading branch information
ericchiang authored Oct 6, 2016
2 parents 84143ac + 2909929 commit 182f14f
Show file tree
Hide file tree
Showing 18 changed files with 642 additions and 44 deletions.
3 changes: 2 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Documentation

Storage

- [ ] Add SQL storage implementation
- [x] Add SQL storage implementation
- [ ] Utilize fixes for third party resources in Kubernetes 1.4

UX
Expand All @@ -48,3 +48,4 @@ Backend

- [ ] Improve logging, possibly switch to logrus
- [ ] Standardize OAuth2 error handling
- [ ] Switch to github.com/ghodss/yaml for []byte to base64 string logic
40 changes: 40 additions & 0 deletions cmd/dex/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"encoding/base64"
"fmt"

"github.com/coreos/dex/connector"
Expand All @@ -26,7 +27,46 @@ type Config struct {

Templates server.TemplateConfig `yaml:"templates"`

// StaticClients cause the server to use this list of clients rather than
// querying the storage. Write operations, like creating a client, will fail.
StaticClients []storage.Client `yaml:"staticClients"`

// If enabled, the server will maintain a list of passwords which can be used
// to identify a user.
EnablePasswordDB bool `yaml:"enablePasswordDB"`

// StaticPasswords cause the server use this list of passwords rather than
// querying the storage. Cannot be specified without enabling a passwords
// database.
//
// The "password" type is identical to the storage.Password type, but does
// unmarshaling into []byte correctly.
StaticPasswords []password `yaml:"staticPasswords"`
}

type password struct {
Email string `yaml:"email"`
Username string `yaml:"username"`
UserID string `yaml:"userID"`

// Because our YAML parser doesn't base64, we have to do it ourselves.
//
// TODO(ericchiang): switch to github.com/ghodss/yaml
Hash string `yaml:"hash"`
}

// decode the hash appropriately and convert to the storage passwords.
func (p password) toPassword() (storage.Password, error) {
hash, err := base64.StdEncoding.DecodeString(p.Hash)
if err != nil {
return storage.Password{}, fmt.Errorf("decoding hash: %v", err)
}
return storage.Password{
Email: p.Email,
Username: p.Username,
UserID: p.UserID,
Hash: hash,
}, nil
}

// OAuth2 describes enabled OAuth2 extensions.
Expand Down
13 changes: 12 additions & 1 deletion cmd/dex/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ func serve(cmd *cobra.Command, args []string) error {
errMsg string
}{
{c.Issuer == "", "no issuer specified in config file"},
{len(c.Connectors) == 0, "no connectors supplied in config file"},
{len(c.Connectors) == 0 && !c.EnablePasswordDB, "no connectors supplied in config file"},
{!c.EnablePasswordDB && len(c.StaticPasswords) != 0, "cannot specify static passwords without enabling password db"},
{c.Storage.Config == nil, "no storage suppied in config file"},
{c.Web.HTTP == "" && c.Web.HTTPS == "", "must supply a HTTP/HTTPS address to listen on"},
{c.Web.HTTPS != "" && c.Web.TLSCert == "", "no cert specified for HTTPS"},
Expand Down Expand Up @@ -103,13 +104,23 @@ func serve(cmd *cobra.Command, args []string) error {
if len(c.StaticClients) > 0 {
s = storage.WithStaticClients(s, c.StaticClients)
}
if len(c.StaticPasswords) > 0 {
p := make([]storage.Password, len(c.StaticPasswords))
for i, pw := range c.StaticPasswords {
if p[i], err = pw.toPassword(); err != nil {
return err
}
}
s = storage.WithStaticPasswords(s, p)
}

serverConfig := server.Config{
SupportedResponseTypes: c.OAuth2.ResponseTypes,
Issuer: c.Issuer,
Connectors: connectors,
Storage: s,
TemplateConfig: c.Templates,
EnablePasswordDB: c.EnablePasswordDB,
}

serv, err := server.NewServer(serverConfig)
Expand Down
19 changes: 13 additions & 6 deletions examples/config-dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,23 @@ connectors:
- type: mockCallback
id: mock-callback
name: Mock
- type: mockPassword
id: mock-password
name: Password
config:
username: "admin"
password: "PASSWORD"

# Instead of reading from an external storage, use this list of clients.
staticClients:
- id: example-app
redirectURIs:
- 'http://127.0.0.1:5555/callback'
name: 'Example App'
secret: ZXhhbXBsZS1hcHAtc2VjcmV0

# Let dex keep a list of passwords which can be used to login the user.
enablePasswordDB: true

# A static list of passwords to login the end user. By identifying here, dex
# won't look in its undlying storage for passwords.
staticPasswords:
- email: "admin@example.com"
# bcrypt hash of the string "password"
hash: "JDJhJDE0JDh4TnlVZ3pzSmVuQm4ySlRPT2QvbmVGcUlnQzF4TEFVRFA3VlpTVzhDNWlkLnFPcmNlYUJX"
username: "admin"
userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"
9 changes: 9 additions & 0 deletions examples/k8s/thirdpartyresources.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,12 @@ kind: ThirdPartyResource
description: "Refresh tokens for clients to continuously act on behalf of an end user."
versions:
- name: v1
---

metadata:
name: password.passwords.oidc.coreos.com
apiVersion: extensions/v1beta1
kind: ThirdPartyResource
description: "Passwords managed by the OIDC server."
versions:
- name: v1
45 changes: 45 additions & 0 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ package server
import (
"errors"
"fmt"
"log"
"net/http"
"net/url"
"path"
"sync/atomic"
"time"

"golang.org/x/crypto/bcrypt"

"github.com/gorilla/mux"

"github.com/coreos/dex/connector"
Expand Down Expand Up @@ -44,6 +47,8 @@ type Config struct {
// If specified, the server will use this function for determining time.
Now func() time.Time

EnablePasswordDB bool

TemplateConfig TemplateConfig
}

Expand Down Expand Up @@ -91,6 +96,14 @@ func newServer(c Config, rotationStrategy rotationStrategy) (*Server, error) {
if err != nil {
return nil, fmt.Errorf("server: can't parse issuer URL")
}
if c.EnablePasswordDB {
c.Connectors = append(c.Connectors, Connector{
ID: "local",
DisplayName: "Email",
Connector: newPasswordDB(c.Storage),
})
}

if len(c.Connectors) == 0 {
return nil, errors.New("server: no connectors specified")
}
Expand Down Expand Up @@ -182,6 +195,38 @@ func (s *Server) absURL(pathItems ...string) string {
return u.String()
}

func newPasswordDB(s storage.Storage) interface {
connector.Connector
connector.PasswordConnector
} {
return passwordDB{s}
}

type passwordDB struct {
s storage.Storage
}

func (db passwordDB) Close() error { return nil }

func (db passwordDB) Login(email, password string) (connector.Identity, bool, error) {
p, err := db.s.GetPassword(email)
if err != nil {
if err != storage.ErrNotFound {
log.Printf("get password: %v", err)
}
return connector.Identity{}, false, err
}
if err := bcrypt.CompareHashAndPassword(p.Hash, []byte(password)); err != nil {
return connector.Identity{}, false, nil
}
return connector.Identity{
UserID: p.UserID,
Username: p.Username,
Email: p.Email,
EmailVerified: true,
}, true, nil
}

// newKeyCacher returns a storage which caches keys so long as the next
func newKeyCacher(s storage.Storage, now func() time.Time) storage.Storage {
if now == nil {
Expand Down
88 changes: 88 additions & 0 deletions server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ import (
"time"

"github.com/ericchiang/oidc"
"github.com/kylelemons/godebug/pretty"
"golang.org/x/crypto/bcrypt"
"golang.org/x/net/context"
"golang.org/x/oauth2"

"github.com/coreos/dex/connector"
"github.com/coreos/dex/connector/mock"
"github.com/coreos/dex/storage"
"github.com/coreos/dex/storage/memory"
Expand Down Expand Up @@ -381,6 +384,91 @@ func TestOAuth2ImplicitFlow(t *testing.T) {
}
}

func TestPasswordDB(t *testing.T) {
s := memory.New()
conn := newPasswordDB(s)
defer conn.Close()

pw := "hi"

h, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.MinCost)
if err != nil {
t.Fatal(err)
}

s.CreatePassword(storage.Password{
Email: "jane@example.com",
Username: "jane",
UserID: "foobar",
Hash: h,
})

tests := []struct {
name string
username string
password string
wantIdentity connector.Identity
wantInvalid bool
wantErr bool
}{
{
name: "valid password",
username: "jane@example.com",
password: pw,
wantIdentity: connector.Identity{
Email: "jane@example.com",
Username: "jane",
UserID: "foobar",
EmailVerified: true,
},
},
{
name: "unknown user",
username: "john@example.com",
password: pw,
wantErr: true,
},
{
name: "invalid password",
username: "jane@example.com",
password: "not the correct password",
wantInvalid: true,
},
}

for _, tc := range tests {
ident, valid, err := conn.Login(tc.username, tc.password)
if err != nil {
if !tc.wantErr {
t.Errorf("%s: %v", tc.name, err)
}
continue
}

if tc.wantErr {
t.Errorf("%s: expected error", tc.name)
continue
}

if !valid {
if !tc.wantInvalid {
t.Errorf("%s: expected valid password", tc.name)
}
continue
}

if tc.wantInvalid {
t.Errorf("%s: expected invalid password", tc.name)
continue
}

if diff := pretty.Compare(tc.wantIdentity, ident); diff != "" {
t.Errorf("%s: %s", tc.name, diff)
}
}

}

type storageWithKeysTrigger struct {
storage.Storage
f func()
Expand Down
Loading

0 comments on commit 182f14f

Please sign in to comment.