connector: add RefreshConnector interface

This commit is contained in:
Eric Chiang
2016-11-18 13:40:41 -08:00
parent 27fb7c523e
commit 952e0f81f5
9 changed files with 438 additions and 191 deletions

View File

@@ -179,7 +179,13 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
authReqID := r.FormValue("req")
// TODO(ericchiang): cache user identity.
authReq, err := s.storage.GetAuthRequest(authReqID)
if err != nil {
log.Printf("Failed to get auth request: %v", err)
s.renderError(w, http.StatusInternalServerError, errServerError, "")
return
}
scopes := parseScopes(authReq.Scopes)
switch r.Method {
case "GET":
@@ -199,7 +205,7 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
// Use the auth request ID as the "state" token.
//
// TODO(ericchiang): Is this appropriate or should we also be using a nonce?
callbackURL, err := conn.LoginURL(s.absURL("/callback"), authReqID)
callbackURL, err := conn.LoginURL(scopes, s.absURL("/callback"), authReqID)
if err != nil {
log.Printf("Connector %q returned error when creating callback: %v", connID, err)
s.renderError(w, http.StatusInternalServerError, errServerError, "")
@@ -221,7 +227,7 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
username := r.FormValue("login")
password := r.FormValue("password")
identity, ok, err := passwordConnector.Login(username, password)
identity, ok, err := passwordConnector.Login(r.Context(), scopes, username, password)
if err != nil {
log.Printf("Failed to login user: %v", err)
s.renderError(w, http.StatusInternalServerError, errServerError, "")
@@ -231,12 +237,6 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
s.templates.password(w, authReqID, r.URL.String(), username, true)
return
}
authReq, err := s.storage.GetAuthRequest(authReqID)
if err != nil {
log.Printf("Failed to get auth request: %v", err)
s.renderError(w, http.StatusInternalServerError, errServerError, "")
return
}
redirectURL, err := s.finalizeLogin(identity, authReq, conn.Connector)
if err != nil {
log.Printf("Failed to finalize login: %v", err)
@@ -286,7 +286,7 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request)
return
}
identity, err := callbackConnector.HandleCallback(r)
identity, err := callbackConnector.HandleCallback(parseScopes(authReq.Scopes), r)
if err != nil {
log.Printf("Failed to authenticate: %v", err)
s.renderError(w, http.StatusInternalServerError, errServerError, "")
@@ -304,34 +304,12 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request)
}
func (s *Server) finalizeLogin(identity connector.Identity, authReq storage.AuthRequest, conn connector.Connector) (string, error) {
if authReq.ConnectorID == "" {
}
claims := storage.Claims{
UserID: identity.UserID,
Username: identity.Username,
Email: identity.Email,
EmailVerified: identity.EmailVerified,
}
groupsConn, ok := conn.(connector.GroupsConnector)
if ok {
reqGroups := func() bool {
for _, scope := range authReq.Scopes {
if scope == scopeGroups {
return true
}
}
return false
}()
if reqGroups {
groups, err := groupsConn.Groups(identity)
if err != nil {
return "", fmt.Errorf("getting groups: %v", err)
}
claims.Groups = groups
}
Groups: identity.Groups,
}
updater := func(a storage.AuthRequest) (storage.AuthRequest, error) {
@@ -407,14 +385,15 @@ func (s *Server) sendCodeResponse(w http.ResponseWriter, r *http.Request, authRe
switch responseType {
case responseTypeCode:
code := storage.AuthCode{
ID: storage.NewID(),
ClientID: authReq.ClientID,
ConnectorID: authReq.ConnectorID,
Nonce: authReq.Nonce,
Scopes: authReq.Scopes,
Claims: authReq.Claims,
Expiry: s.now().Add(time.Minute * 30),
RedirectURI: authReq.RedirectURI,
ID: storage.NewID(),
ClientID: authReq.ClientID,
ConnectorID: authReq.ConnectorID,
Nonce: authReq.Nonce,
Scopes: authReq.Scopes,
Claims: authReq.Claims,
Expiry: s.now().Add(time.Minute * 30),
RedirectURI: authReq.RedirectURI,
ConnectorData: authReq.ConnectorData,
}
if err := s.storage.CreateAuthCode(code); err != nil {
log.Printf("Failed to create auth code: %v", err)
@@ -537,12 +516,13 @@ func (s *Server) handleAuthCode(w http.ResponseWriter, r *http.Request, client s
var refreshToken string
if reqRefresh {
refresh := storage.RefreshToken{
RefreshToken: storage.NewID(),
ClientID: authCode.ClientID,
ConnectorID: authCode.ConnectorID,
Scopes: authCode.Scopes,
Claims: authCode.Claims,
Nonce: authCode.Nonce,
RefreshToken: storage.NewID(),
ClientID: authCode.ClientID,
ConnectorID: authCode.ConnectorID,
Scopes: authCode.Scopes,
Claims: authCode.Claims,
Nonce: authCode.Nonce,
ConnectorData: authCode.ConnectorData,
}
if err := s.storage.CreateRefresh(refresh); err != nil {
log.Printf("failed to create refresh token: %v", err)
@@ -574,6 +554,10 @@ func (s *Server) handleRefreshToken(w http.ResponseWriter, r *http.Request, clie
return
}
// Per the OAuth2 spec, if the client has omitted the scopes, default to the original
// authorized scopes.
//
// https://tools.ietf.org/html/rfc6749#section-6
scopes := refresh.Scopes
if scope != "" {
requestedScopes := strings.Fields(scope)
@@ -601,7 +585,43 @@ func (s *Server) handleRefreshToken(w http.ResponseWriter, r *http.Request, clie
scopes = requestedScopes
}
// TODO(ericchiang): re-auth with backends
conn, ok := s.connectors[refresh.ConnectorID]
if !ok {
log.Printf("connector ID not found: %q", refresh.ConnectorID)
tokenErr(w, errServerError, "", http.StatusInternalServerError)
return
}
// Can the connector refresh the identity? If so, attempt to refresh the data
// in the connector.
//
// TODO(ericchiang): We may want a strict mode where connectors that don't implement
// this interface can't perform refreshing.
if refreshConn, ok := conn.Connector.(connector.RefreshConnector); ok {
ident := connector.Identity{
UserID: refresh.Claims.UserID,
Username: refresh.Claims.Username,
Email: refresh.Claims.Email,
EmailVerified: refresh.Claims.EmailVerified,
Groups: refresh.Claims.Groups,
ConnectorData: refresh.ConnectorData,
}
ident, err := refreshConn.Refresh(r.Context(), parseScopes(scopes), ident)
if err != nil {
log.Printf("failed to refresh identity: %v", err)
tokenErr(w, errServerError, "", http.StatusInternalServerError)
return
}
// Update the claims of the refresh token.
//
// UserID intentionally ignored for now.
refresh.Claims.Username = ident.Username
refresh.Claims.Email = ident.Email
refresh.Claims.EmailVerified = ident.EmailVerified
refresh.Claims.Groups = ident.Groups
refresh.ConnectorData = ident.ConnectorData
}
idToken, expiry, err := s.newIDToken(client.ID, refresh.Claims, scopes, refresh.Nonce)
if err != nil {
@@ -610,6 +630,8 @@ func (s *Server) handleRefreshToken(w http.ResponseWriter, r *http.Request, clie
return
}
// Refresh tokens are claimed exactly once. Delete the current token and
// create a new one.
if err := s.storage.DeleteRefresh(code); err != nil {
log.Printf("failed to delete auth code: %v", err)
tokenErr(w, errServerError, "", http.StatusInternalServerError)

View File

@@ -10,6 +10,7 @@ import (
"strings"
"time"
"github.com/coreos/dex/connector"
"github.com/coreos/dex/storage"
)
@@ -93,6 +94,19 @@ const (
responseTypeIDToken = "id_token" // ID Token in url fragment
)
func parseScopes(scopes []string) connector.Scopes {
var s connector.Scopes
for _, scope := range scopes {
switch scope {
case scopeOfflineAccess:
s.OfflineAccess = true
case scopeGroups:
s.Groups = true
}
}
return s
}
type audience []string
func (a audience) MarshalJSON() ([]byte, error) {

View File

@@ -211,9 +211,7 @@ type passwordDB struct {
s storage.Storage
}
func (db passwordDB) Close() error { return nil }
func (db passwordDB) Login(email, password string) (connector.Identity, bool, error) {
func (db passwordDB) Login(ctx context.Context, s connector.Scopes, email, password string) (connector.Identity, bool, error) {
p, err := db.s.GetPassword(email)
if err != nil {
if err != storage.ErrNotFound {
@@ -233,6 +231,31 @@ func (db passwordDB) Login(email, password string) (connector.Identity, bool, er
}, true, nil
}
func (db passwordDB) Refresh(ctx context.Context, s connector.Scopes, identity connector.Identity) (connector.Identity, error) {
// If the user has been deleted, the refresh token will be rejected.
p, err := db.s.GetPassword(identity.Email)
if err != nil {
if err == storage.ErrNotFound {
return connector.Identity{}, errors.New("user not found")
}
return connector.Identity{}, fmt.Errorf("get password: %v", err)
}
// User removed but a new user with the same email exists.
if p.UserID != identity.UserID {
return connector.Identity{}, errors.New("user not found")
}
// If a user has updated their username, that will be reflected in the
// refreshed token.
//
// No other fields are expected to be refreshable as email is effectively used
// as an ID and this implementation doesn't deal with groups.
identity.Username = p.Username
return identity, 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 {

View File

@@ -662,7 +662,6 @@ func TestCrossClientScopes(t *testing.T) {
func TestPasswordDB(t *testing.T) {
s := memory.New()
conn := newPasswordDB(s)
defer conn.Close()
pw := "hi"
@@ -712,7 +711,7 @@ func TestPasswordDB(t *testing.T) {
}
for _, tc := range tests {
ident, valid, err := conn.Login(tc.username, tc.password)
ident, valid, err := conn.Login(context.Background(), connector.Scopes{}, tc.username, tc.password)
if err != nil {
if !tc.wantErr {
t.Errorf("%s: %v", tc.name, err)