*: connectors use a different identity object than storage

This commit is contained in:
Eric Chiang 2016-08-02 21:14:24 -07:00
parent e716c14718
commit f4c5722e42
7 changed files with 121 additions and 95 deletions

View File

@ -1,11 +1,7 @@
// Package connector defines interfaces for federated identity strategies. // Package connector defines interfaces for federated identity strategies.
package connector package connector
import ( import "net/http"
"net/http"
"github.com/coreos/poke/storage"
)
// Connector is a mechanism for federating login to a remote identity service. // Connector is a mechanism for federating login to a remote identity service.
// //
@ -15,18 +11,32 @@ type Connector interface {
Close() error 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. // PasswordConnector is an optional interface for password based connectors.
type PasswordConnector interface { 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. // CallbackConnector is an optional interface for callback based connectors.
type CallbackConnector interface { type CallbackConnector interface {
LoginURL(callbackURL, state string) (string, error) 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. // GroupsConnector is an optional interface for connectors which can map a user to groups.
type GroupsConnector interface { type GroupsConnector interface {
Groups(identity storage.Identity) ([]string, error) Groups(identity Identity) ([]string, error)
} }

View File

@ -14,7 +14,6 @@ import (
"golang.org/x/oauth2/github" "golang.org/x/oauth2/github"
"github.com/coreos/poke/connector" "github.com/coreos/poke/connector"
"github.com/coreos/poke/storage"
) )
const baseURL = "https://api.github.com" const baseURL = "https://api.github.com"
@ -85,7 +84,7 @@ func (e *oauth2Error) Error() string {
return e.error + ": " + e.errorDescription 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() q := r.URL.Query()
if errType := q.Get("error"); errType != "" { if errType := q.Get("error"); errType != "" {
return identity, "", &oauth2Error{errType, q.Get("error_description")} return identity, "", &oauth2Error{errType, q.Get("error_description")}
@ -128,7 +127,7 @@ func (c *githubConnector) HandleCallback(r *http.Request) (identity storage.Iden
if username == "" { if username == "" {
username = user.Login username = user.Login
} }
identity = storage.Identity{ identity = connector.Identity{
UserID: strconv.Itoa(user.ID), UserID: strconv.Itoa(user.ID),
Username: username, Username: username,
Email: user.Email, Email: user.Email,
@ -138,7 +137,7 @@ func (c *githubConnector) HandleCallback(r *http.Request) (identity storage.Iden
return identity, q.Get("state"), nil 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 var data connectorData
if err := json.Unmarshal(identity.ConnectorData, &data); err != nil { if err := json.Unmarshal(identity.ConnectorData, &data); err != nil {
return nil, fmt.Errorf("decode connector data: %v", err) return nil, fmt.Errorf("decode connector data: %v", err)

View File

@ -8,7 +8,6 @@ import (
"gopkg.in/ldap.v2" "gopkg.in/ldap.v2"
"github.com/coreos/poke/connector" "github.com/coreos/poke/connector"
"github.com/coreos/poke/storage"
) )
// Config holds the configuration parameters for the LDAP connector. // Config holds the configuration parameters for the LDAP connector.
@ -32,6 +31,8 @@ type ldapConnector struct {
Config Config
} }
var _ connector.PasswordConnector = (*ldapConnector)(nil)
func (c *ldapConnector) do(f func(c *ldap.Conn) error) error { func (c *ldapConnector) do(f func(c *ldap.Conn) error) error {
// TODO(ericchiang): Connection pooling. // TODO(ericchiang): Connection pooling.
conn, err := ldap.Dial("tcp", c.Host) 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) 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 { err := c.do(func(conn *ldap.Conn) error {
return conn.Bind(fmt.Sprintf("uid=%s,%s", username, c.BindDN), password) return conn.Bind(fmt.Sprintf("uid=%s,%s", username, c.BindDN), password)
}) })
if err != nil { 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 { func (c *ldapConnector) Close() error {

View File

@ -2,12 +2,13 @@
package mock package mock
import ( import (
"bytes"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"github.com/coreos/poke/connector" "github.com/coreos/poke/connector"
"github.com/coreos/poke/storage"
) )
// New returns a mock connector which requires no user interaction. It always returns // New returns a mock connector which requires no user interaction. It always returns
@ -16,6 +17,11 @@ func New() connector.Connector {
return mockConnector{} return mockConnector{}
} }
var (
_ connector.CallbackConnector = mockConnector{}
_ connector.GroupsConnector = mockConnector{}
)
type mockConnector struct{} type mockConnector struct{}
func (m mockConnector) Close() error { return nil } func (m mockConnector) Close() error { return nil }
@ -31,16 +37,22 @@ func (m mockConnector) LoginURL(callbackURL, state string) (string, error) {
return u.String(), nil return u.String(), nil
} }
func (m mockConnector) HandleCallback(r *http.Request) (storage.Identity, string, error) { var connectorData = []byte("foobar")
return storage.Identity{
func (m mockConnector) HandleCallback(r *http.Request) (connector.Identity, string, error) {
return connector.Identity{
UserID: "0-385-28089-0", UserID: "0-385-28089-0",
Username: "Kilgore Trout", Username: "Kilgore Trout",
Email: "kilgore@kilgore.trout", Email: "kilgore@kilgore.trout",
EmailVerified: true, EmailVerified: true,
ConnectorData: connectorData,
}, r.URL.Query().Get("state"), nil }, 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 return []string{"authors"}, nil
} }

View File

@ -180,17 +180,14 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
renderPasswordTmpl(w, state, r.URL.String(), "Invalid credentials") renderPasswordTmpl(w, state, r.URL.String(), "Invalid credentials")
return return
} }
redirectURL, err := s.finalizeLogin(identity, state, connID, conn.Connector)
groups, ok, err := s.groups(identity, state, conn.Connector)
if err != nil { if err != nil {
log.Printf("Failed to finalize login: %v", err)
s.renderError(w, http.StatusInternalServerError, errServerError, "") s.renderError(w, http.StatusInternalServerError, errServerError, "")
return return
} }
if ok {
identity.Groups = groups
}
s.redirectToApproval(w, r, identity, connID, state) http.Redirect(w, r, redirectURL, http.StatusSeeOther)
default: default:
s.notFound(w, r) s.notFound(w, r)
} }
@ -215,54 +212,56 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request)
s.renderError(w, http.StatusInternalServerError, errServerError, "") s.renderError(w, http.StatusInternalServerError, errServerError, "")
return return
} }
groups, ok, err := s.groups(identity, state, conn.Connector)
redirectURL, err := s.finalizeLogin(identity, state, connID, conn.Connector)
if err != nil { if err != nil {
log.Printf("Failed to finalize login: %v", err)
s.renderError(w, http.StatusInternalServerError, errServerError, "") s.renderError(w, http.StatusInternalServerError, errServerError, "")
return return
} }
if ok {
identity.Groups = groups http.Redirect(w, r, redirectURL, http.StatusSeeOther)
}
s.redirectToApproval(w, r, identity, connID, state)
} }
func (s *Server) redirectToApproval(w http.ResponseWriter, r *http.Request, identity storage.Identity, connectorID, state string) { func (s *Server) finalizeLogin(identity connector.Identity, authReqID, connectorID string, conn connector.Connector) (string, error) {
updater := func(a storage.AuthRequest) (storage.AuthRequest, error) { claims := storage.Identity{
a.Identity = &identity UserID: identity.UserID,
a.ConnectorID = connectorID Username: identity.Username,
return a, nil Email: identity.Email,
EmailVerified: identity.EmailVerified,
} }
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
}
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) groupsConn, ok := conn.(connector.GroupsConnector)
if !ok { if ok {
return nil, false, nil authReq, err := s.storage.GetAuthRequest(authReqID)
} if err != nil {
authReq, err := s.storage.GetAuthRequest(authReqID) return "", fmt.Errorf("get auth request: %v", err)
if err != nil { }
log.Printf("get auth request: %v", err) reqGroups := func() bool {
return nil, false, err for _, scope := range authReq.Scopes {
} if scope == scopeGroups {
reqGroups := func() bool { return true
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) { func (s *Server) handleApproval(w http.ResponseWriter, r *http.Request) {

View File

@ -77,8 +77,6 @@ type Identity struct {
Email string `json:"email"` Email string `json:"email"`
EmailVerified bool `json:"emailVerified"` EmailVerified bool `json:"emailVerified"`
Groups []string `json:"groups,omitempty"` Groups []string `json:"groups,omitempty"`
ConnectorData []byte `json:"connectorData,omitempty"`
} }
func fromStorageIdentity(i storage.Identity) Identity { func fromStorageIdentity(i storage.Identity) Identity {
@ -88,7 +86,6 @@ func fromStorageIdentity(i storage.Identity) Identity {
Email: i.Email, Email: i.Email,
EmailVerified: i.EmailVerified, EmailVerified: i.EmailVerified,
Groups: i.Groups, Groups: i.Groups,
ConnectorData: i.ConnectorData,
} }
} }
@ -99,7 +96,6 @@ func toStorageIdentity(i Identity) storage.Identity {
Email: i.Email, Email: i.Email,
EmailVerified: i.EmailVerified, EmailVerified: i.EmailVerified,
Groups: i.Groups, Groups: i.Groups,
ConnectorData: i.ConnectorData,
} }
} }
@ -126,7 +122,8 @@ type AuthRequest struct {
// with a backend. // with a backend.
Identity *Identity `json:"identity,omitempty"` Identity *Identity `json:"identity,omitempty"`
// The connector used to login the user. Set when the user authenticates. // 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"` Expiry time.Time `json:"expiry"`
} }
@ -149,6 +146,7 @@ func toStorageAuthRequest(req AuthRequest) storage.AuthRequest {
State: req.State, State: req.State,
ForceApprovalPrompt: req.ForceApprovalPrompt, ForceApprovalPrompt: req.ForceApprovalPrompt,
ConnectorID: req.ConnectorID, ConnectorID: req.ConnectorID,
ConnectorData: req.ConnectorData,
Expiry: req.Expiry, Expiry: req.Expiry,
} }
if req.Identity != nil { if req.Identity != nil {
@ -176,6 +174,7 @@ func (cli *client) fromStorageAuthRequest(a storage.AuthRequest) AuthRequest {
State: a.State, State: a.State,
ForceApprovalPrompt: a.ForceApprovalPrompt, ForceApprovalPrompt: a.ForceApprovalPrompt,
ConnectorID: a.ConnectorID, ConnectorID: a.ConnectorID,
ConnectorData: a.ConnectorData,
Expiry: a.Expiry, Expiry: a.Expiry,
} }
if a.Identity != nil { if a.Identity != nil {
@ -198,8 +197,10 @@ type AuthCode struct {
Nonce string `json:"nonce,omitempty"` Nonce string `json:"nonce,omitempty"`
State string `json:"state,omitempty"` State string `json:"state,omitempty"`
Identity Identity `json:"identity,omitempty"` Identity Identity `json:"identity,omitempty"`
ConnectorID string `json:"connectorID,omitempty"`
ConnectorID string `json:"connectorID,omitempty"`
ConnectorData []byte `json:"connectorData,omitempty"`
Expiry time.Time `json:"expiry"` Expiry time.Time `json:"expiry"`
} }
@ -221,26 +222,28 @@ func (cli *client) fromStorageAuthCode(a storage.AuthCode) AuthCode {
Name: a.ID, Name: a.ID,
Namespace: cli.namespace, Namespace: cli.namespace,
}, },
ClientID: a.ClientID, ClientID: a.ClientID,
RedirectURI: a.RedirectURI, RedirectURI: a.RedirectURI,
ConnectorID: a.ConnectorID, ConnectorID: a.ConnectorID,
Nonce: a.Nonce, ConnectorData: a.ConnectorData,
Scopes: a.Scopes, Nonce: a.Nonce,
Identity: fromStorageIdentity(a.Identity), Scopes: a.Scopes,
Expiry: a.Expiry, Identity: fromStorageIdentity(a.Identity),
Expiry: a.Expiry,
} }
} }
func toStorageAuthCode(a AuthCode) storage.AuthCode { func toStorageAuthCode(a AuthCode) storage.AuthCode {
return storage.AuthCode{ return storage.AuthCode{
ID: a.ObjectMeta.Name, ID: a.ObjectMeta.Name,
ClientID: a.ClientID, ClientID: a.ClientID,
RedirectURI: a.RedirectURI, RedirectURI: a.RedirectURI,
ConnectorID: a.ConnectorID, ConnectorID: a.ConnectorID,
Nonce: a.Nonce, ConnectorData: a.ConnectorData,
Scopes: a.Scopes, Nonce: a.Nonce,
Identity: toStorageIdentity(a.Identity), Scopes: a.Scopes,
Expiry: a.Expiry, Identity: toStorageIdentity(a.Identity),
Expiry: a.Expiry,
} }
} }

View File

@ -104,12 +104,6 @@ type Identity struct {
EmailVerified bool EmailVerified bool
Groups []string 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 // 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 // The identity of the end user. Generally nil until the user authenticates
// with a backend. // with a backend.
Identity *Identity 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 Expiry time.Time
} }
@ -145,7 +142,9 @@ type AuthCode struct {
ClientID string ClientID string
RedirectURI string RedirectURI string
ConnectorID string
ConnectorID string
ConnectorData []byte
Nonce string Nonce string
@ -162,8 +161,10 @@ type Refresh struct {
RefreshToken string RefreshToken string
// Client this refresh token is valid for. // Client this refresh token is valid for.
ClientID string ClientID string
ConnectorID string
ConnectorID string
ConnectorData []byte
// Scopes present in the initial request. Refresh requests may specify a set // Scopes present in the initial request. Refresh requests may specify a set
// of scopes different from the initial request when refreshing a token, // of scopes different from the initial request when refreshing a token,