diff --git a/connector/connector.go b/connector/connector.go
index 4cdbcae48427e8a365c4efdc21fe46950763abe7..8235caae4120ed2d27ed40a3cc2aa7f539571ba1 100644
--- a/connector/connector.go
+++ b/connector/connector.go
@@ -1,11 +1,7 @@
 // Package connector defines interfaces for federated identity strategies.
 package connector
 
-import (
-	"net/http"
-
-	"github.com/coreos/poke/storage"
-)
+import "net/http"
 
 // Connector is a mechanism for federating login to a remote identity service.
 //
@@ -15,18 +11,32 @@ type Connector interface {
 	Close() error
 }
 
+// Identity represents the ID Token claims supported by the server.
+type Identity struct {
+	UserID        string
+	Username      string
+	Email         string
+	EmailVerified bool
+
+	// ConnectorData holds data used by the connector for subsequent requests after initial
+	// authentication, such as access tokens for upstream provides.
+	//
+	// This data is never shared with end users, OAuth clients, or through the API.
+	ConnectorData []byte
+}
+
 // PasswordConnector is an optional interface for password based connectors.
 type PasswordConnector interface {
-	Login(username, password string) (identity storage.Identity, validPassword bool, err error)
+	Login(username, password string) (identity Identity, validPassword bool, err error)
 }
 
 // CallbackConnector is an optional interface for callback based connectors.
 type CallbackConnector interface {
 	LoginURL(callbackURL, state string) (string, error)
-	HandleCallback(r *http.Request) (identity storage.Identity, state string, err error)
+	HandleCallback(r *http.Request) (identity Identity, state string, err error)
 }
 
 // GroupsConnector is an optional interface for connectors which can map a user to groups.
 type GroupsConnector interface {
-	Groups(identity storage.Identity) ([]string, error)
+	Groups(identity Identity) ([]string, error)
 }
diff --git a/connector/github/github.go b/connector/github/github.go
index 3dbd2f05aa5b6dece8c58bd48c9903b3abe7f569..2546f16bf5d62521d0cdef4d0b2633e0111969d7 100644
--- a/connector/github/github.go
+++ b/connector/github/github.go
@@ -14,7 +14,6 @@ import (
 	"golang.org/x/oauth2/github"
 
 	"github.com/coreos/poke/connector"
-	"github.com/coreos/poke/storage"
 )
 
 const baseURL = "https://api.github.com"
@@ -85,7 +84,7 @@ func (e *oauth2Error) Error() string {
 	return e.error + ": " + e.errorDescription
 }
 
-func (c *githubConnector) HandleCallback(r *http.Request) (identity storage.Identity, state string, err error) {
+func (c *githubConnector) HandleCallback(r *http.Request) (identity connector.Identity, state string, err error) {
 	q := r.URL.Query()
 	if errType := q.Get("error"); errType != "" {
 		return identity, "", &oauth2Error{errType, q.Get("error_description")}
@@ -128,7 +127,7 @@ func (c *githubConnector) HandleCallback(r *http.Request) (identity storage.Iden
 	if username == "" {
 		username = user.Login
 	}
-	identity = storage.Identity{
+	identity = connector.Identity{
 		UserID:        strconv.Itoa(user.ID),
 		Username:      username,
 		Email:         user.Email,
@@ -138,7 +137,7 @@ func (c *githubConnector) HandleCallback(r *http.Request) (identity storage.Iden
 	return identity, q.Get("state"), nil
 }
 
-func (c *githubConnector) Groups(identity storage.Identity) ([]string, error) {
+func (c *githubConnector) Groups(identity connector.Identity) ([]string, error) {
 	var data connectorData
 	if err := json.Unmarshal(identity.ConnectorData, &data); err != nil {
 		return nil, fmt.Errorf("decode connector data: %v", err)
diff --git a/connector/ldap/ldap.go b/connector/ldap/ldap.go
index 21c4fce89ba31c11c8806c59d795ecf9f3c0f299..746b410ce95f2b6a639af540820f79869ad7dc53 100644
--- a/connector/ldap/ldap.go
+++ b/connector/ldap/ldap.go
@@ -8,7 +8,6 @@ import (
 	"gopkg.in/ldap.v2"
 
 	"github.com/coreos/poke/connector"
-	"github.com/coreos/poke/storage"
 )
 
 // Config holds the configuration parameters for the LDAP connector.
@@ -32,6 +31,8 @@ type ldapConnector struct {
 	Config
 }
 
+var _ connector.PasswordConnector = (*ldapConnector)(nil)
+
 func (c *ldapConnector) do(f func(c *ldap.Conn) error) error {
 	// TODO(ericchiang): Connection pooling.
 	conn, err := ldap.Dial("tcp", c.Host)
@@ -43,15 +44,16 @@ func (c *ldapConnector) do(f func(c *ldap.Conn) error) error {
 	return f(conn)
 }
 
-func (c *ldapConnector) Login(username, password string) (storage.Identity, error) {
+func (c *ldapConnector) Login(username, password string) (connector.Identity, bool, error) {
 	err := c.do(func(conn *ldap.Conn) error {
 		return conn.Bind(fmt.Sprintf("uid=%s,%s", username, c.BindDN), password)
 	})
 	if err != nil {
-		return storage.Identity{}, err
+		// TODO(ericchiang): Determine when the user has entered invalid credentials.
+		return connector.Identity{}, false, err
 	}
 
-	return storage.Identity{Username: username}, nil
+	return connector.Identity{Username: username}, true, nil
 }
 
 func (c *ldapConnector) Close() error {
diff --git a/connector/mock/connectortest.go b/connector/mock/connectortest.go
index 4c7a821d894bbe213a3472aa1cf133d320c7a0d1..216681a7d8b7e488f83a55c03ea1916a67b26839 100644
--- a/connector/mock/connectortest.go
+++ b/connector/mock/connectortest.go
@@ -2,12 +2,13 @@
 package mock
 
 import (
+	"bytes"
+	"errors"
 	"fmt"
 	"net/http"
 	"net/url"
 
 	"github.com/coreos/poke/connector"
-	"github.com/coreos/poke/storage"
 )
 
 // New returns a mock connector which requires no user interaction. It always returns
@@ -16,6 +17,11 @@ func New() connector.Connector {
 	return mockConnector{}
 }
 
+var (
+	_ connector.CallbackConnector = mockConnector{}
+	_ connector.GroupsConnector   = mockConnector{}
+)
+
 type mockConnector struct{}
 
 func (m mockConnector) Close() error { return nil }
@@ -31,16 +37,22 @@ func (m mockConnector) LoginURL(callbackURL, state string) (string, error) {
 	return u.String(), nil
 }
 
-func (m mockConnector) HandleCallback(r *http.Request) (storage.Identity, string, error) {
-	return storage.Identity{
+var connectorData = []byte("foobar")
+
+func (m mockConnector) HandleCallback(r *http.Request) (connector.Identity, string, error) {
+	return connector.Identity{
 		UserID:        "0-385-28089-0",
 		Username:      "Kilgore Trout",
 		Email:         "kilgore@kilgore.trout",
 		EmailVerified: true,
+		ConnectorData: connectorData,
 	}, r.URL.Query().Get("state"), nil
 }
 
-func (m mockConnector) Groups(identity storage.Identity) ([]string, error) {
+func (m mockConnector) Groups(identity connector.Identity) ([]string, error) {
+	if !bytes.Equal(identity.ConnectorData, connectorData) {
+		return nil, errors.New("connector data mismatch")
+	}
 	return []string{"authors"}, nil
 }
 
diff --git a/server/handlers.go b/server/handlers.go
index 8a555391b58e89a5d9e69b8c80bbe0f8f7144666..6ce2f6e554c9103a544acca3aa4b6dacd6569cea 100644
--- a/server/handlers.go
+++ b/server/handlers.go
@@ -180,17 +180,14 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
 			renderPasswordTmpl(w, state, r.URL.String(), "Invalid credentials")
 			return
 		}
-
-		groups, ok, err := s.groups(identity, state, conn.Connector)
+		redirectURL, err := s.finalizeLogin(identity, state, connID, conn.Connector)
 		if err != nil {
+			log.Printf("Failed to finalize login: %v", err)
 			s.renderError(w, http.StatusInternalServerError, errServerError, "")
 			return
 		}
-		if ok {
-			identity.Groups = groups
-		}
 
-		s.redirectToApproval(w, r, identity, connID, state)
+		http.Redirect(w, r, redirectURL, http.StatusSeeOther)
 	default:
 		s.notFound(w, r)
 	}
@@ -215,54 +212,56 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request)
 		s.renderError(w, http.StatusInternalServerError, errServerError, "")
 		return
 	}
-	groups, ok, err := s.groups(identity, state, conn.Connector)
+
+	redirectURL, err := s.finalizeLogin(identity, state, connID, conn.Connector)
 	if err != nil {
+		log.Printf("Failed to finalize login: %v", err)
 		s.renderError(w, http.StatusInternalServerError, errServerError, "")
 		return
 	}
-	if ok {
-		identity.Groups = groups
-	}
-	s.redirectToApproval(w, r, identity, connID, state)
+
+	http.Redirect(w, r, redirectURL, http.StatusSeeOther)
 }
 
-func (s *Server) redirectToApproval(w http.ResponseWriter, r *http.Request, identity storage.Identity, connectorID, state string) {
-	updater := func(a storage.AuthRequest) (storage.AuthRequest, error) {
-		a.Identity = &identity
-		a.ConnectorID = connectorID
-		return a, nil
-	}
-	if err := s.storage.UpdateAuthRequest(state, updater); err != nil {
-		log.Printf("Failed to updated auth request with identity: %v", err)
-		s.renderError(w, http.StatusInternalServerError, errServerError, "")
-		return
+func (s *Server) finalizeLogin(identity connector.Identity, authReqID, connectorID string, conn connector.Connector) (string, error) {
+	claims := storage.Identity{
+		UserID:        identity.UserID,
+		Username:      identity.Username,
+		Email:         identity.Email,
+		EmailVerified: identity.EmailVerified,
 	}
-	http.Redirect(w, r, path.Join(s.issuerURL.Path, "/approval")+"?state="+state, http.StatusSeeOther)
-}
 
-func (s *Server) groups(identity storage.Identity, authReqID string, conn connector.Connector) ([]string, bool, error) {
 	groupsConn, ok := conn.(connector.GroupsConnector)
-	if !ok {
-		return nil, false, nil
-	}
-	authReq, err := s.storage.GetAuthRequest(authReqID)
-	if err != nil {
-		log.Printf("get auth request: %v", err)
-		return nil, false, err
-	}
-	reqGroups := func() bool {
-		for _, scope := range authReq.Scopes {
-			if scope == scopeGroups {
-				return true
+	if ok {
+		authReq, err := s.storage.GetAuthRequest(authReqID)
+		if err != nil {
+			return "", fmt.Errorf("get auth request: %v", err)
+		}
+		reqGroups := func() bool {
+			for _, scope := range authReq.Scopes {
+				if scope == scopeGroups {
+					return true
+				}
+			}
+			return false
+		}()
+		if reqGroups {
+			if claims.Groups, err = groupsConn.Groups(identity); err != nil {
+				return "", fmt.Errorf("getting groups: %v", err)
 			}
 		}
-		return false
-	}()
-	if !reqGroups {
-		return nil, false, nil
 	}
-	groups, err := groupsConn.Groups(identity)
-	return groups, true, err
+
+	updater := func(a storage.AuthRequest) (storage.AuthRequest, error) {
+		a.Identity = &claims
+		a.ConnectorID = connectorID
+		a.ConnectorData = identity.ConnectorData
+		return a, nil
+	}
+	if err := s.storage.UpdateAuthRequest(authReqID, updater); err != nil {
+		return "", fmt.Errorf("failed to update auth request: %v", err)
+	}
+	return path.Join(s.issuerURL.Path, "/approval") + "?state=" + authReqID, nil
 }
 
 func (s *Server) handleApproval(w http.ResponseWriter, r *http.Request) {
diff --git a/storage/kubernetes/types.go b/storage/kubernetes/types.go
index 9124f205b3d10814eb484934c1ee25b8549778c5..f9f4a0f333ba4577f60e0753a2576501a63e17db 100644
--- a/storage/kubernetes/types.go
+++ b/storage/kubernetes/types.go
@@ -77,8 +77,6 @@ type Identity struct {
 	Email         string   `json:"email"`
 	EmailVerified bool     `json:"emailVerified"`
 	Groups        []string `json:"groups,omitempty"`
-
-	ConnectorData []byte `json:"connectorData,omitempty"`
 }
 
 func fromStorageIdentity(i storage.Identity) Identity {
@@ -88,7 +86,6 @@ func fromStorageIdentity(i storage.Identity) Identity {
 		Email:         i.Email,
 		EmailVerified: i.EmailVerified,
 		Groups:        i.Groups,
-		ConnectorData: i.ConnectorData,
 	}
 }
 
@@ -99,7 +96,6 @@ func toStorageIdentity(i Identity) storage.Identity {
 		Email:         i.Email,
 		EmailVerified: i.EmailVerified,
 		Groups:        i.Groups,
-		ConnectorData: i.ConnectorData,
 	}
 }
 
@@ -126,7 +122,8 @@ type AuthRequest struct {
 	// with a backend.
 	Identity *Identity `json:"identity,omitempty"`
 	// The connector used to login the user. Set when the user authenticates.
-	ConnectorID string `json:"connectorID,omitempty"`
+	ConnectorID   string `json:"connectorID,omitempty"`
+	ConnectorData []byte `json:"connectorData,omitempty"`
 
 	Expiry time.Time `json:"expiry"`
 }
@@ -149,6 +146,7 @@ func toStorageAuthRequest(req AuthRequest) storage.AuthRequest {
 		State:               req.State,
 		ForceApprovalPrompt: req.ForceApprovalPrompt,
 		ConnectorID:         req.ConnectorID,
+		ConnectorData:       req.ConnectorData,
 		Expiry:              req.Expiry,
 	}
 	if req.Identity != nil {
@@ -176,6 +174,7 @@ func (cli *client) fromStorageAuthRequest(a storage.AuthRequest) AuthRequest {
 		State:               a.State,
 		ForceApprovalPrompt: a.ForceApprovalPrompt,
 		ConnectorID:         a.ConnectorID,
+		ConnectorData:       a.ConnectorData,
 		Expiry:              a.Expiry,
 	}
 	if a.Identity != nil {
@@ -198,8 +197,10 @@ type AuthCode struct {
 	Nonce string `json:"nonce,omitempty"`
 	State string `json:"state,omitempty"`
 
-	Identity    Identity `json:"identity,omitempty"`
-	ConnectorID string   `json:"connectorID,omitempty"`
+	Identity Identity `json:"identity,omitempty"`
+
+	ConnectorID   string `json:"connectorID,omitempty"`
+	ConnectorData []byte `json:"connectorData,omitempty"`
 
 	Expiry time.Time `json:"expiry"`
 }
@@ -221,26 +222,28 @@ func (cli *client) fromStorageAuthCode(a storage.AuthCode) AuthCode {
 			Name:      a.ID,
 			Namespace: cli.namespace,
 		},
-		ClientID:    a.ClientID,
-		RedirectURI: a.RedirectURI,
-		ConnectorID: a.ConnectorID,
-		Nonce:       a.Nonce,
-		Scopes:      a.Scopes,
-		Identity:    fromStorageIdentity(a.Identity),
-		Expiry:      a.Expiry,
+		ClientID:      a.ClientID,
+		RedirectURI:   a.RedirectURI,
+		ConnectorID:   a.ConnectorID,
+		ConnectorData: a.ConnectorData,
+		Nonce:         a.Nonce,
+		Scopes:        a.Scopes,
+		Identity:      fromStorageIdentity(a.Identity),
+		Expiry:        a.Expiry,
 	}
 }
 
 func toStorageAuthCode(a AuthCode) storage.AuthCode {
 	return storage.AuthCode{
-		ID:          a.ObjectMeta.Name,
-		ClientID:    a.ClientID,
-		RedirectURI: a.RedirectURI,
-		ConnectorID: a.ConnectorID,
-		Nonce:       a.Nonce,
-		Scopes:      a.Scopes,
-		Identity:    toStorageIdentity(a.Identity),
-		Expiry:      a.Expiry,
+		ID:            a.ObjectMeta.Name,
+		ClientID:      a.ClientID,
+		RedirectURI:   a.RedirectURI,
+		ConnectorID:   a.ConnectorID,
+		ConnectorData: a.ConnectorData,
+		Nonce:         a.Nonce,
+		Scopes:        a.Scopes,
+		Identity:      toStorageIdentity(a.Identity),
+		Expiry:        a.Expiry,
 	}
 }
 
diff --git a/storage/storage.go b/storage/storage.go
index 32b65b6cefd718a5bd3067345250ead97e8f32ef..536d9a9608db7fb57db66de95bbfd4bdc06fdac3 100644
--- a/storage/storage.go
+++ b/storage/storage.go
@@ -104,12 +104,6 @@ type Identity struct {
 	EmailVerified bool
 
 	Groups []string
-
-	// ConnectorData holds data used by the connector for subsequent requests after initial
-	// authentication, such as access tokens for upstream provides.
-	//
-	// This data is never shared with end users, OAuth clients, or through the API.
-	ConnectorData []byte
 }
 
 // AuthRequest represents a OAuth2 client authorization request. It holds the state
@@ -133,8 +127,11 @@ type AuthRequest struct {
 	// The identity of the end user. Generally nil until the user authenticates
 	// with a backend.
 	Identity *Identity
-	// The connector used to login the user. Set when the user authenticates.
-	ConnectorID string
+
+	// The connector used to login the user and any data the connector wishes to persists.
+	// Set when the user authenticates.
+	ConnectorID   string
+	ConnectorData []byte
 
 	Expiry time.Time
 }
@@ -145,7 +142,9 @@ type AuthCode struct {
 
 	ClientID    string
 	RedirectURI string
-	ConnectorID string
+
+	ConnectorID   string
+	ConnectorData []byte
 
 	Nonce string
 
@@ -162,8 +161,10 @@ type Refresh struct {
 	RefreshToken string
 
 	// Client this refresh token is valid for.
-	ClientID    string
-	ConnectorID string
+	ClientID string
+
+	ConnectorID   string
+	ConnectorData []byte
 
 	// Scopes present in the initial request. Refresh requests may specify a set
 	// of scopes different from the initial request when refreshing a token,