diff --git a/TODO.md b/TODO.md
index 58196109056868bb4b42efa523d3681c3557714d..03e9bc525435d0c5ddf766df2dea60fc3d4f7716 100644
--- a/TODO.md
+++ b/TODO.md
@@ -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
@@ -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
diff --git a/cmd/dex/config.go b/cmd/dex/config.go
index f50cd173edb528198c33009ed98f7fba6c852c60..482075f388981c92ef30516654f28d4ad768cceb 100644
--- a/cmd/dex/config.go
+++ b/cmd/dex/config.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"encoding/base64"
 	"fmt"
 
 	"github.com/coreos/dex/connector"
@@ -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.
diff --git a/cmd/dex/serve.go b/cmd/dex/serve.go
index 08309e0f5f3e977f560bc5214f19b65c9db5a4f4..0e35e6af8018e6bb4da7114b54bee8402b71757d 100644
--- a/cmd/dex/serve.go
+++ b/cmd/dex/serve.go
@@ -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"},
@@ -103,6 +104,15 @@ 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,
@@ -110,6 +120,7 @@ func serve(cmd *cobra.Command, args []string) error {
 		Connectors:             connectors,
 		Storage:                s,
 		TemplateConfig:         c.Templates,
+		EnablePasswordDB:       c.EnablePasswordDB,
 	}
 
 	serv, err := server.NewServer(serverConfig)
diff --git a/examples/config-dev.yaml b/examples/config-dev.yaml
index 2a2736b5058f362ba68bc1271615c7e8e3d7265b..d771bb7ca9a26e27cdce3ba4a4035813dd9284f6 100644
--- a/examples/config-dev.yaml
+++ b/examples/config-dev.yaml
@@ -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"
diff --git a/examples/k8s/thirdpartyresources.yaml b/examples/k8s/thirdpartyresources.yaml
index 65e0225649f805850ab812a05364c969e922219d..40f03027259236120fa1b854c4b5d9b24d7fbe31 100644
--- a/examples/k8s/thirdpartyresources.yaml
+++ b/examples/k8s/thirdpartyresources.yaml
@@ -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
diff --git a/server/server.go b/server/server.go
index 81b86ec314e7cc032fecdbdfd8a165a504bf3281..703af6688d1005d4ef3152e399a966c639138d83 100644
--- a/server/server.go
+++ b/server/server.go
@@ -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"
@@ -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
 }
 
@@ -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")
 	}
@@ -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 {
diff --git a/server/server_test.go b/server/server_test.go
index 296f22ccca78a2d1f3b18d6f4e5ade78754eaf9d..46fcc7108268324001bb0fe9bd38bf980ad5afbd 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -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"
@@ -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()
diff --git a/storage/conformance/conformance.go b/storage/conformance/conformance.go
index 9ba2ec6878b97a415544ccc6186e9b539a65c7bf..a2458680cbfb94b65c6a6cd877c5945ab51fb2e9 100644
--- a/storage/conformance/conformance.go
+++ b/storage/conformance/conformance.go
@@ -8,6 +8,8 @@ import (
 	"testing"
 	"time"
 
+	"golang.org/x/crypto/bcrypt"
+
 	"github.com/coreos/dex/storage"
 
 	"github.com/kylelemons/godebug/pretty"
@@ -30,6 +32,7 @@ func RunTestSuite(t *testing.T, sf StorageFactory) {
 		{"AuthRequestCRUD", testAuthRequestCRUD},
 		{"ClientCRUD", testClientCRUD},
 		{"RefreshTokenCRUD", testRefreshTokenCRUD},
+		{"PasswordCRUD", testPasswordCRUD},
 	}
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
@@ -222,5 +225,54 @@ func testRefreshTokenCRUD(t *testing.T, s storage.Storage) {
 	if _, err := s.GetRefresh(id); err != storage.ErrNotFound {
 		t.Errorf("after deleting refresh expected storage.ErrNotFound, got %v", err)
 	}
+}
 
+func testPasswordCRUD(t *testing.T, s storage.Storage) {
+	// Use bcrypt.MinCost to keep the tests short.
+	passwordHash, err := bcrypt.GenerateFromPassword([]byte("secret"), bcrypt.MinCost)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	password := storage.Password{
+		Email:    "jane@example.com",
+		Hash:     passwordHash,
+		Username: "jane",
+		UserID:   "foobar",
+	}
+	if err := s.CreatePassword(password); err != nil {
+		t.Fatalf("create password token: %v", err)
+	}
+
+	getAndCompare := func(id string, want storage.Password) {
+		gr, err := s.GetPassword(id)
+		if err != nil {
+			t.Errorf("get password %q: %v", id, err)
+			return
+		}
+		if diff := pretty.Compare(want, gr); diff != "" {
+			t.Errorf("password retrieved from storage did not match: %s", diff)
+		}
+	}
+
+	getAndCompare("jane@example.com", password)
+	getAndCompare("JANE@example.com", password) // Emails should be case insensitive
+
+	if err := s.UpdatePassword(password.Email, func(old storage.Password) (storage.Password, error) {
+		old.Username = "jane doe"
+		return old, nil
+	}); err != nil {
+		t.Fatalf("failed to update auth request: %v", err)
+	}
+
+	password.Username = "jane doe"
+	getAndCompare("jane@example.com", password)
+
+	if err := s.DeletePassword(password.Email); err != nil {
+		t.Fatalf("failed to delete password: %v", err)
+	}
+
+	if _, err := s.GetPassword(password.Email); err != storage.ErrNotFound {
+		t.Errorf("after deleting password expected storage.ErrNotFound, got %v", err)
+	}
 }
diff --git a/storage/kubernetes/client.go b/storage/kubernetes/client.go
index f6f97f17d2ae6c5137c0371d3f9338951e8b2139..ec703214b3d22180251796f5fd989e1c66bff067 100644
--- a/storage/kubernetes/client.go
+++ b/storage/kubernetes/client.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"crypto/tls"
 	"crypto/x509"
+	"encoding/base64"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -165,6 +166,26 @@ func (c *client) delete(resource, name string) error {
 	return checkHTTPErr(resp, http.StatusOK)
 }
 
+func (c *client) deleteAll(resource string) error {
+	var list struct {
+		k8sapi.TypeMeta `json:",inline"`
+		k8sapi.ListMeta `json:"metadata,omitempty"`
+		Items           []struct {
+			k8sapi.TypeMeta   `json:",inline"`
+			k8sapi.ObjectMeta `json:"metadata,omitempty"`
+		} `json:"items"`
+	}
+	if err := c.list(resource, &list); err != nil {
+		return err
+	}
+	for _, item := range list.Items {
+		if err := c.delete(resource, item.Name); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
 func (c *client) put(resource, name string, v interface{}) error {
 	body, err := json.Marshal(v)
 	if err != nil {
@@ -190,9 +211,9 @@ func (c *client) put(resource, name string, v interface{}) error {
 
 func newClient(cluster k8sapi.Cluster, user k8sapi.AuthInfo, namespace string) (*client, error) {
 	tlsConfig := cryptopasta.DefaultTLSConfig()
-	data := func(b []byte, file string) ([]byte, error) {
-		if b != nil {
-			return b, nil
+	data := func(b string, file string) ([]byte, error) {
+		if b != "" {
+			return base64.StdEncoding.DecodeString(b)
 		}
 		if file == "" {
 			return nil, nil
diff --git a/storage/kubernetes/k8sapi/client.go b/storage/kubernetes/k8sapi/client.go
index c8df73416ecdff113c0aca0cb8f1728c66137ed5..d84fa5ccee3bab76a9e5c7bf2bd152a8dcec70e4 100644
--- a/storage/kubernetes/k8sapi/client.go
+++ b/storage/kubernetes/k8sapi/client.go
@@ -62,7 +62,9 @@ type Cluster struct {
 	// CertificateAuthority is the path to a cert file for the certificate authority.
 	CertificateAuthority string `yaml:"certificate-authority,omitempty"`
 	// CertificateAuthorityData contains PEM-encoded certificate authority certificates. Overrides CertificateAuthority
-	CertificateAuthorityData []byte `yaml:"certificate-authority-data,omitempty"`
+	//
+	// NOTE(ericchiang): Our yaml parser doesn't assume []byte is a base64 encoded string.
+	CertificateAuthorityData string `yaml:"certificate-authority-data,omitempty"`
 	// Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields
 	Extensions []NamedExtension `yaml:"extensions,omitempty"`
 }
@@ -72,11 +74,15 @@ type AuthInfo struct {
 	// ClientCertificate is the path to a client cert file for TLS.
 	ClientCertificate string `yaml:"client-certificate,omitempty"`
 	// ClientCertificateData contains PEM-encoded data from a client cert file for TLS. Overrides ClientCertificate
-	ClientCertificateData []byte `yaml:"client-certificate-data,omitempty"`
+	//
+	// NOTE(ericchiang): Our yaml parser doesn't assume []byte is a base64 encoded string.
+	ClientCertificateData string `yaml:"client-certificate-data,omitempty"`
 	// ClientKey is the path to a client key file for TLS.
 	ClientKey string `yaml:"client-key,omitempty"`
 	// ClientKeyData contains PEM-encoded data from a client key file for TLS. Overrides ClientKey
-	ClientKeyData []byte `yaml:"client-key-data,omitempty"`
+	//
+	// NOTE(ericchiang): Our yaml parser doesn't assume []byte is a base64 encoded string.
+	ClientKeyData string `yaml:"client-key-data,omitempty"`
 	// Token is the bearer token for authentication to the kubernetes cluster.
 	Token string `yaml:"token,omitempty"`
 	// Impersonate is the username to imperonate.  The name matches the flag.
diff --git a/storage/kubernetes/storage.go b/storage/kubernetes/storage.go
index bfb488556635c0ffd43afeb25134eb1e23ca84d0..44920f6b261b03f66f9e66e8c531e4cf98da7a6a 100644
--- a/storage/kubernetes/storage.go
+++ b/storage/kubernetes/storage.go
@@ -20,6 +20,7 @@ const (
 	kindClient       = "OAuth2Client"
 	kindRefreshToken = "RefreshToken"
 	kindKeys         = "SigningKey"
+	kindPassword     = "Password"
 )
 
 const (
@@ -28,6 +29,7 @@ const (
 	resourceClient       = "oauth2clients"
 	resourceRefreshToken = "refreshtokens"
 	resourceKeys         = "signingkeies" // Kubernetes attempts to pluralize.
+	resourcePassword     = "passwords"
 )
 
 // Config values for the Kubernetes storage type.
@@ -109,6 +111,10 @@ func (cli *client) CreateAuthCode(c storage.AuthCode) error {
 	return cli.post(resourceAuthCode, cli.fromStorageAuthCode(c))
 }
 
+func (cli *client) CreatePassword(p storage.Password) error {
+	return cli.post(resourcePassword, cli.fromStoragePassword(p))
+}
+
 func (cli *client) CreateRefresh(r storage.RefreshToken) error {
 	refresh := RefreshToken{
 		TypeMeta: k8sapi.TypeMeta{
@@ -152,6 +158,14 @@ func (cli *client) GetClient(id string) (storage.Client, error) {
 	return toStorageClient(c), nil
 }
 
+func (cli *client) GetPassword(email string) (storage.Password, error) {
+	var p Password
+	if err := cli.get(resourcePassword, emailToID(email), &p); err != nil {
+		return storage.Password{}, err
+	}
+	return toStoragePassword(p), nil
+}
+
 func (cli *client) GetKeys() (storage.Keys, error) {
 	var keys Keys
 	if err := cli.get(resourceKeys, keysName, &keys); err != nil {
@@ -199,6 +213,10 @@ func (cli *client) DeleteRefresh(id string) error {
 	return cli.delete(resourceRefreshToken, id)
 }
 
+func (cli *client) DeletePassword(email string) error {
+	return cli.delete(resourcePassword, emailToID(email))
+}
+
 func (cli *client) UpdateClient(id string, updater func(old storage.Client) (storage.Client, error)) error {
 	var c Client
 	if err := cli.get(resourceClient, id, &c); err != nil {
@@ -214,6 +232,23 @@ func (cli *client) UpdateClient(id string, updater func(old storage.Client) (sto
 	return cli.put(resourceClient, id, newClient)
 }
 
+func (cli *client) UpdatePassword(email string, updater func(old storage.Password) (storage.Password, error)) error {
+	id := emailToID(email)
+	var p Password
+	if err := cli.get(resourcePassword, id, &p); err != nil {
+		return err
+	}
+
+	updated, err := updater(toStoragePassword(p))
+	if err != nil {
+		return err
+	}
+
+	newPassword := cli.fromStoragePassword(updated)
+	newPassword.ObjectMeta = p.ObjectMeta
+	return cli.put(resourcePassword, id, newPassword)
+}
+
 func (cli *client) UpdateKeys(updater func(old storage.Keys) (storage.Keys, error)) error {
 	firstUpdate := false
 	var keys Keys
diff --git a/storage/kubernetes/storage_test.go b/storage/kubernetes/storage_test.go
index c0011f39cc4958756a402ad53715ccb68bf0b04d..f41b01b1d0b90c409047f38d598f921f2fcb62bc 100644
--- a/storage/kubernetes/storage_test.go
+++ b/storage/kubernetes/storage_test.go
@@ -75,7 +75,18 @@ func TestURLFor(t *testing.T) {
 func TestStorage(t *testing.T) {
 	client := loadClient(t)
 	conformance.RunTestSuite(t, func() storage.Storage {
-		// TODO(erichiang): Tear down namespaces between each iteration.
+		for _, resource := range []string{
+			resourceAuthCode,
+			resourceAuthRequest,
+			resourceClient,
+			resourceRefreshToken,
+			resourceKeys,
+			resourcePassword,
+		} {
+			if err := client.deleteAll(resource); err != nil {
+				t.Fatalf("delete all %q failed: %v", resource, err)
+			}
+		}
 		return client
 	})
 }
diff --git a/storage/kubernetes/types.go b/storage/kubernetes/types.go
index 8bc934f2ebdf090869e2b2f804cbb8bf62ea49f6..3c914e844a4ce1c6c8ade9c0aa6be743f0c6b8e1 100644
--- a/storage/kubernetes/types.go
+++ b/storage/kubernetes/types.go
@@ -1,6 +1,8 @@
 package kubernetes
 
 import (
+	"encoding/base32"
+	"strings"
 	"time"
 
 	jose "gopkg.in/square/go-jose.v2"
@@ -182,6 +184,60 @@ func (cli *client) fromStorageAuthRequest(a storage.AuthRequest) AuthRequest {
 	return req
 }
 
+// Password is a mirrored struct from the stroage with JSON struct tags and
+// Kubernetes type metadata.
+type Password struct {
+	k8sapi.TypeMeta   `json:",inline"`
+	k8sapi.ObjectMeta `json:"metadata,omitempty"`
+
+	// The Kubernetes name is actually an encoded version of this value.
+	//
+	// This field is IMMUTABLE. Do not change.
+	Email string `json:"email,omitempty"`
+
+	Hash     []byte `json:"hash,omitempty"`
+	Username string `json:"username,omitempty"`
+	UserID   string `json:"userID,omitempty"`
+}
+
+// Kubernetes only allows lower case letters for names.
+//
+// NOTE(ericchiang): This is currently copied from the storage package's NewID()
+// method. Once we refactor those into the storage, just use that instead.
+var encoding = base32.NewEncoding("abcdefghijklmnopqrstuvwxyz234567")
+
+// Map an arbitrary email to a valid Kuberntes name.
+func emailToID(email string) string {
+	return strings.TrimRight(encoding.EncodeToString([]byte(strings.ToLower(email))), "=")
+}
+
+func (cli *client) fromStoragePassword(p storage.Password) Password {
+	email := strings.ToLower(p.Email)
+	return Password{
+		TypeMeta: k8sapi.TypeMeta{
+			Kind:       kindPassword,
+			APIVersion: cli.apiVersionForResource(resourcePassword),
+		},
+		ObjectMeta: k8sapi.ObjectMeta{
+			Name:      emailToID(email),
+			Namespace: cli.namespace,
+		},
+		Email:    email,
+		Hash:     p.Hash,
+		Username: p.Username,
+		UserID:   p.UserID,
+	}
+}
+
+func toStoragePassword(p Password) storage.Password {
+	return storage.Password{
+		Email:    p.Email,
+		Hash:     p.Hash,
+		Username: p.Username,
+		UserID:   p.UserID,
+	}
+}
+
 // AuthCode is a mirrored struct from storage with JSON struct tags and
 // Kubernetes type metadata.
 type AuthCode struct {
diff --git a/storage/memory/memory.go b/storage/memory/memory.go
index d178f20b8b505040bbdc69588f0f44570c36c3f3..b85d68dfc5d7e5e2cd4e3b27450987d0d46526e6 100644
--- a/storage/memory/memory.go
+++ b/storage/memory/memory.go
@@ -2,7 +2,7 @@
 package memory
 
 import (
-	"errors"
+	"strings"
 	"sync"
 
 	"github.com/coreos/dex/storage"
@@ -15,6 +15,7 @@ func New() storage.Storage {
 		authCodes:     make(map[string]storage.AuthCode),
 		refreshTokens: make(map[string]storage.RefreshToken),
 		authReqs:      make(map[string]storage.AuthRequest),
+		passwords:     make(map[string]storage.Password),
 	}
 }
 
@@ -37,6 +38,7 @@ type memStorage struct {
 	authCodes     map[string]storage.AuthCode
 	refreshTokens map[string]storage.RefreshToken
 	authReqs      map[string]storage.AuthRequest
+	passwords     map[string]storage.Password
 
 	keys storage.Keys
 }
@@ -47,28 +49,73 @@ func (s *memStorage) tx(f func()) {
 	f()
 }
 
-var errAlreadyExists = errors.New("already exists")
-
 func (s *memStorage) Close() error { return nil }
 
-func (s *memStorage) CreateClient(c storage.Client) error {
-	s.tx(func() { s.clients[c.ID] = c })
-	return nil
+func (s *memStorage) CreateClient(c storage.Client) (err error) {
+	s.tx(func() {
+		if _, ok := s.clients[c.ID]; ok {
+			err = storage.ErrAlreadyExists
+		} else {
+			s.clients[c.ID] = c
+		}
+	})
+	return
 }
 
-func (s *memStorage) CreateAuthCode(c storage.AuthCode) error {
-	s.tx(func() { s.authCodes[c.ID] = c })
-	return nil
+func (s *memStorage) CreateAuthCode(c storage.AuthCode) (err error) {
+	s.tx(func() {
+		if _, ok := s.authCodes[c.ID]; ok {
+			err = storage.ErrAlreadyExists
+		} else {
+			s.authCodes[c.ID] = c
+		}
+	})
+	return
 }
 
-func (s *memStorage) CreateRefresh(r storage.RefreshToken) error {
-	s.tx(func() { s.refreshTokens[r.RefreshToken] = r })
-	return nil
+func (s *memStorage) CreateRefresh(r storage.RefreshToken) (err error) {
+	s.tx(func() {
+		if _, ok := s.refreshTokens[r.RefreshToken]; ok {
+			err = storage.ErrAlreadyExists
+		} else {
+			s.refreshTokens[r.RefreshToken] = r
+		}
+	})
+	return
 }
 
-func (s *memStorage) CreateAuthRequest(a storage.AuthRequest) error {
-	s.tx(func() { s.authReqs[a.ID] = a })
-	return nil
+func (s *memStorage) CreateAuthRequest(a storage.AuthRequest) (err error) {
+	s.tx(func() {
+		if _, ok := s.authReqs[a.ID]; ok {
+			err = storage.ErrAlreadyExists
+		} else {
+			s.authReqs[a.ID] = a
+		}
+	})
+	return
+}
+
+func (s *memStorage) CreatePassword(p storage.Password) (err error) {
+	p.Email = strings.ToLower(p.Email)
+	s.tx(func() {
+		if _, ok := s.passwords[p.Email]; ok {
+			err = storage.ErrAlreadyExists
+		} else {
+			s.passwords[p.Email] = p
+		}
+	})
+	return
+}
+
+func (s *memStorage) GetPassword(email string) (p storage.Password, err error) {
+	email = strings.ToLower(email)
+	s.tx(func() {
+		var ok bool
+		if p, ok = s.passwords[email]; !ok {
+			err = storage.ErrNotFound
+		}
+	})
+	return
 }
 
 func (s *memStorage) GetClient(id string) (client storage.Client, err error) {
@@ -126,6 +173,18 @@ func (s *memStorage) ListRefreshTokens() (tokens []storage.RefreshToken, err err
 	return
 }
 
+func (s *memStorage) DeletePassword(email string) (err error) {
+	email = strings.ToLower(email)
+	s.tx(func() {
+		if _, ok := s.passwords[email]; !ok {
+			err = storage.ErrNotFound
+			return
+		}
+		delete(s.passwords, email)
+	})
+	return
+}
+
 func (s *memStorage) DeleteClient(id string) (err error) {
 	s.tx(func() {
 		if _, ok := s.clients[id]; !ok {
@@ -235,9 +294,24 @@ func (s *memStorage) UpdateAuthRequest(id string, updater func(old storage.AuthR
 			err = storage.ErrNotFound
 			return
 		}
-		if req, err := updater(req); err == nil {
+		if req, err = updater(req); err == nil {
 			s.authReqs[id] = req
 		}
 	})
 	return
 }
+
+func (s *memStorage) UpdatePassword(email string, updater func(p storage.Password) (storage.Password, error)) (err error) {
+	email = strings.ToLower(email)
+	s.tx(func() {
+		req, ok := s.passwords[email]
+		if !ok {
+			err = storage.ErrNotFound
+			return
+		}
+		if req, err = updater(req); err == nil {
+			s.passwords[email] = req
+		}
+	})
+	return
+}
diff --git a/storage/sql/crud.go b/storage/sql/crud.go
index 532b864841d4a9540d1d9bc446adc273e6b2b71b..ca941f7ccd15d54d02cddfe25d02b7babc54645d 100644
--- a/storage/sql/crud.go
+++ b/storage/sql/crud.go
@@ -6,6 +6,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"strings"
 
 	"github.com/coreos/dex/storage"
 )
@@ -137,7 +138,7 @@ func (c *conn) UpdateAuthRequest(id string, updater func(a storage.AuthRequest)
 			a.Claims.UserID, a.Claims.Username, a.Claims.Email, a.Claims.EmailVerified,
 			encoder(a.Claims.Groups),
 			a.ConnectorID, a.ConnectorData,
-			a.Expiry, a.ID,
+			a.Expiry, r.ID,
 		)
 		if err != nil {
 			return fmt.Errorf("update auth request: %v", err)
@@ -462,14 +463,83 @@ func scanClient(s scanner) (cli storage.Client, err error) {
 	return cli, nil
 }
 
-func (c *conn) DeleteAuthRequest(id string) error { return c.delete("auth_request", id) }
-func (c *conn) DeleteAuthCode(id string) error    { return c.delete("auth_code", id) }
-func (c *conn) DeleteClient(id string) error      { return c.delete("client", id) }
-func (c *conn) DeleteRefresh(id string) error     { return c.delete("refresh_token", id) }
+func (c *conn) CreatePassword(p storage.Password) error {
+	p.Email = strings.ToLower(p.Email)
+	_, err := c.Exec(`
+		insert into password (
+			email, hash, username, user_id
+		)
+		values (
+			$1, $2, $3, $4
+		);
+	`,
+		p.Email, p.Hash, p.Username, p.UserID,
+	)
+	if err != nil {
+		return fmt.Errorf("insert password: %v", err)
+	}
+	return nil
+}
+
+func (c *conn) UpdatePassword(email string, updater func(p storage.Password) (storage.Password, error)) error {
+	return c.ExecTx(func(tx *trans) error {
+		p, err := getPassword(tx, email)
+		if err != nil {
+			return err
+		}
+
+		np, err := updater(p)
+		if err != nil {
+			return err
+		}
+		_, err = tx.Exec(`
+			update password
+			set
+				hash = $1, username = $2, user_id = $3
+			where email = $4;
+		`,
+			np.Hash, np.Username, np.UserID, p.Email,
+		)
+		if err != nil {
+			return fmt.Errorf("update password: %v", err)
+		}
+		return nil
+	})
+}
+
+func (c *conn) GetPassword(email string) (storage.Password, error) {
+	return getPassword(c, email)
+}
+
+func getPassword(q querier, email string) (p storage.Password, err error) {
+	email = strings.ToLower(email)
+	err = q.QueryRow(`
+		select
+			email, hash, username, user_id
+		from password where email = $1;
+	`, email).Scan(
+		&p.Email, &p.Hash, &p.Username, &p.UserID,
+	)
+	if err != nil {
+		if err == sql.ErrNoRows {
+			return p, storage.ErrNotFound
+		}
+		return p, fmt.Errorf("select password: %v", err)
+	}
+	return p, nil
+}
+
+func (c *conn) DeleteAuthRequest(id string) error { return c.delete("auth_request", "id", id) }
+func (c *conn) DeleteAuthCode(id string) error    { return c.delete("auth_code", "id", id) }
+func (c *conn) DeleteClient(id string) error      { return c.delete("client", "id", id) }
+func (c *conn) DeleteRefresh(id string) error     { return c.delete("refresh_token", "id", id) }
+func (c *conn) DeletePassword(email string) error {
+	return c.delete("password", "email", strings.ToLower(email))
+}
 
 // Do NOT call directly. Does not escape table.
-func (c *conn) delete(table, id string) error {
-	result, err := c.Exec(`delete from `+table+` where id = $1`, id)
+func (c *conn) delete(table, field, id string) error {
+	result, err := c.Exec(`delete from `+table+` where `+field+` = $1`, id)
 	if err != nil {
 		return fmt.Errorf("delete %s: %v", table, id)
 	}
diff --git a/storage/sql/migrate.go b/storage/sql/migrate.go
index 8754caf5eab33d63b41290c23f12bf9ed937ee37..d9c254d3fc73637d4748e9d0d8a65724044994ee 100644
--- a/storage/sql/migrate.go
+++ b/storage/sql/migrate.go
@@ -137,6 +137,13 @@ var migrations = []migration{
 				connector_id text not null,
 				connector_data bytea
 			);
+
+			create table password (
+				email text not null primary key,
+				hash bytea not null,
+				username text not null,
+				user_id text not null
+			);
 		
 			-- keys is a weird table because we only ever expect there to be a single row
 			create table keys (
diff --git a/storage/static_clients.go b/storage/static.go
similarity index 58%
rename from storage/static_clients.go
rename to storage/static.go
index d793239341174c679b7d6f3e3075e4e8adcf58d5..8274c5f80482cd8ce722109798a2222b37b41063 100644
--- a/storage/static_clients.go
+++ b/storage/static.go
@@ -1,6 +1,9 @@
 package storage
 
-import "errors"
+import (
+	"errors"
+	"strings"
+)
 
 // Tests for this code are in the "memory" package, since this package doesn't
 // define a concrete storage implementation.
@@ -53,3 +56,39 @@ func (s staticClientsStorage) DeleteClient(id string) error {
 func (s staticClientsStorage) UpdateClient(id string, updater func(old Client) (Client, error)) error {
 	return errors.New("static clients: read-only cannot update client")
 }
+
+type staticPasswordsStorage struct {
+	Storage
+
+	passwordsByEmail map[string]Password
+}
+
+// WithStaticPasswords returns a storage with a read-only set of passwords. Write actions,
+// such as creating other passwords, will fail.
+func WithStaticPasswords(s Storage, staticPasswords []Password) Storage {
+	passwordsByEmail := make(map[string]Password, len(staticPasswords))
+	for _, p := range staticPasswords {
+		p.Email = strings.ToLower(p.Email)
+		passwordsByEmail[p.Email] = p
+	}
+	return staticPasswordsStorage{s, passwordsByEmail}
+}
+
+func (s staticPasswordsStorage) GetPassword(email string) (Password, error) {
+	if password, ok := s.passwordsByEmail[strings.ToLower(email)]; ok {
+		return password, nil
+	}
+	return Password{}, ErrNotFound
+}
+
+func (s staticPasswordsStorage) CreatePassword(p Password) error {
+	return errors.New("static passwords: read-only cannot create password")
+}
+
+func (s staticPasswordsStorage) DeletePassword(id string) error {
+	return errors.New("static passwords: read-only cannot create password")
+}
+
+func (s staticPasswordsStorage) UpdatePassword(id string, updater func(old Password) (Password, error)) error {
+	return errors.New("static passwords: read-only cannot update password")
+}
diff --git a/storage/storage.go b/storage/storage.go
index 4a92485b5af9ff9f59c99e2eca1f00fe4d7d8d48..78c162f92164b9d168a53defeffb60133598c3b9 100644
--- a/storage/storage.go
+++ b/storage/storage.go
@@ -16,12 +16,12 @@ import (
 )
 
 var (
-	// stubbed out for testing
-	now = time.Now
-)
+	// ErrNotFound is the error returned by storages if a resource cannot be found.
+	ErrNotFound = errors.New("not found")
 
-// ErrNotFound is the error returned by storages if a resource cannot be found.
-var ErrNotFound = errors.New("not found")
+	// ErrAlreadyExists is the error returned by storages if a resource ID is taken during a create.
+	ErrAlreadyExists = errors.New("ID already exists")
+)
 
 // Kubernetes only allows lower case letters for names.
 //
@@ -51,6 +51,7 @@ type Storage interface {
 	CreateClient(c Client) error
 	CreateAuthCode(c AuthCode) error
 	CreateRefresh(r RefreshToken) error
+	CreatePassword(p Password) error
 
 	// TODO(ericchiang): return (T, bool, error) so we can indicate not found
 	// requests that way instead of using ErrNotFound.
@@ -59,6 +60,7 @@ type Storage interface {
 	GetClient(id string) (Client, error)
 	GetKeys() (Keys, error)
 	GetRefresh(id string) (RefreshToken, error)
+	GetPassword(email string) (Password, error)
 
 	ListClients() ([]Client, error)
 	ListRefreshTokens() ([]RefreshToken, error)
@@ -68,6 +70,7 @@ type Storage interface {
 	DeleteAuthCode(code string) error
 	DeleteClient(id string) error
 	DeleteRefresh(id string) error
+	DeletePassword(email string) error
 
 	// Update functions are assumed to be a performed within a single object transaction.
 	//
@@ -75,6 +78,7 @@ type Storage interface {
 	UpdateClient(id string, updater func(old Client) (Client, error)) error
 	UpdateKeys(updater func(old Keys) (Keys, error)) error
 	UpdateAuthRequest(id string, updater func(a AuthRequest) (AuthRequest, error)) error
+	UpdatePassword(email string, updater func(p Password) (Password, error)) error
 
 	// TODO(ericchiang): Add a GarbageCollect(now time.Time) method so conformance tests
 	// can test implementations.
@@ -217,6 +221,28 @@ type RefreshToken struct {
 	Nonce string
 }
 
+// Password is an email to password mapping managed by the storage.
+type Password struct {
+	// Email and identifying name of the password. Emails are assumed to be valid and
+	// determining that an end-user controls the address is left to an outside application.
+	//
+	// Emails are case insensitive and should be standardized by the storage.
+	//
+	// Storages that don't support an extended character set for IDs, such as '.' and '@'
+	// (cough cough, kubernetes), must map this value appropriately.
+	Email string `yaml:"email"`
+
+	// Bcrypt encoded hash of the password. This package recommends a cost value of at
+	// least 14.
+	Hash []byte `yaml:"hash"`
+
+	// Optional username to display. NOT used during login.
+	Username string `yaml:"username"`
+
+	// Randomly generated user ID. This is NOT the primary ID of the Password object.
+	UserID string `yaml:"userID"`
+}
+
 // VerificationKey is a rotated signing key which can still be used to verify
 // signatures.
 type VerificationKey struct {