367 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			367 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package server
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
 | 
						|
	"golang.org/x/crypto/bcrypt"
 | 
						|
 | 
						|
	"github.com/dexidp/dex/api/v2"
 | 
						|
	"github.com/dexidp/dex/pkg/log"
 | 
						|
	"github.com/dexidp/dex/server/internal"
 | 
						|
	"github.com/dexidp/dex/storage"
 | 
						|
	"github.com/dexidp/dex/version"
 | 
						|
)
 | 
						|
 | 
						|
// apiVersion increases every time a new call is added to the API. Clients should use this info
 | 
						|
// to determine if the server supports specific features.
 | 
						|
const apiVersion = 2
 | 
						|
 | 
						|
const (
 | 
						|
	// recCost is the recommended bcrypt cost, which balances hash strength and
 | 
						|
	// efficiency.
 | 
						|
	recCost = 12
 | 
						|
 | 
						|
	// upBoundCost is a sane upper bound on bcrypt cost determined by benchmarking:
 | 
						|
	// high enough to ensure secure encryption, low enough to not put unnecessary
 | 
						|
	// load on a dex server.
 | 
						|
	upBoundCost = 16
 | 
						|
)
 | 
						|
 | 
						|
// NewAPI returns a server which implements the gRPC API interface.
 | 
						|
func NewAPI(s storage.Storage, logger log.Logger) api.DexServer {
 | 
						|
	return dexAPI{
 | 
						|
		s:      s,
 | 
						|
		logger: logger,
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
type dexAPI struct {
 | 
						|
	s      storage.Storage
 | 
						|
	logger log.Logger
 | 
						|
}
 | 
						|
 | 
						|
func (d dexAPI) CreateClient(ctx context.Context, req *api.CreateClientReq) (*api.CreateClientResp, error) {
 | 
						|
	if req.Client == nil {
 | 
						|
		return nil, errors.New("no client supplied")
 | 
						|
	}
 | 
						|
 | 
						|
	if req.Client.Id == "" {
 | 
						|
		req.Client.Id = storage.NewID()
 | 
						|
	}
 | 
						|
	if req.Client.Secret == "" {
 | 
						|
		req.Client.Secret = storage.NewID() + storage.NewID()
 | 
						|
	}
 | 
						|
 | 
						|
	c := storage.Client{
 | 
						|
		ID:           req.Client.Id,
 | 
						|
		Secret:       req.Client.Secret,
 | 
						|
		RedirectURIs: req.Client.RedirectUris,
 | 
						|
		TrustedPeers: req.Client.TrustedPeers,
 | 
						|
		Public:       req.Client.Public,
 | 
						|
		Name:         req.Client.Name,
 | 
						|
		LogoURL:      req.Client.LogoUrl,
 | 
						|
	}
 | 
						|
	if err := d.s.CreateClient(c); err != nil {
 | 
						|
		if err == storage.ErrAlreadyExists {
 | 
						|
			return &api.CreateClientResp{AlreadyExists: true}, nil
 | 
						|
		}
 | 
						|
		d.logger.Errorf("api: failed to create client: %v", err)
 | 
						|
		return nil, fmt.Errorf("create client: %v", err)
 | 
						|
	}
 | 
						|
 | 
						|
	return &api.CreateClientResp{
 | 
						|
		Client: req.Client,
 | 
						|
	}, nil
 | 
						|
}
 | 
						|
 | 
						|
func (d dexAPI) UpdateClient(ctx context.Context, req *api.UpdateClientReq) (*api.UpdateClientResp, error) {
 | 
						|
	if req.Id == "" {
 | 
						|
		return nil, errors.New("update client: no client ID supplied")
 | 
						|
	}
 | 
						|
 | 
						|
	err := d.s.UpdateClient(req.Id, func(old storage.Client) (storage.Client, error) {
 | 
						|
		if req.RedirectUris != nil {
 | 
						|
			old.RedirectURIs = req.RedirectUris
 | 
						|
		}
 | 
						|
		if req.TrustedPeers != nil {
 | 
						|
			old.TrustedPeers = req.TrustedPeers
 | 
						|
		}
 | 
						|
		if req.Name != "" {
 | 
						|
			old.Name = req.Name
 | 
						|
		}
 | 
						|
		if req.LogoUrl != "" {
 | 
						|
			old.LogoURL = req.LogoUrl
 | 
						|
		}
 | 
						|
		return old, nil
 | 
						|
	})
 | 
						|
	if err != nil {
 | 
						|
		if err == storage.ErrNotFound {
 | 
						|
			return &api.UpdateClientResp{NotFound: true}, nil
 | 
						|
		}
 | 
						|
		d.logger.Errorf("api: failed to update the client: %v", err)
 | 
						|
		return nil, fmt.Errorf("update client: %v", err)
 | 
						|
	}
 | 
						|
	return &api.UpdateClientResp{}, nil
 | 
						|
}
 | 
						|
 | 
						|
func (d dexAPI) DeleteClient(ctx context.Context, req *api.DeleteClientReq) (*api.DeleteClientResp, error) {
 | 
						|
	err := d.s.DeleteClient(req.Id)
 | 
						|
	if err != nil {
 | 
						|
		if err == storage.ErrNotFound {
 | 
						|
			return &api.DeleteClientResp{NotFound: true}, nil
 | 
						|
		}
 | 
						|
		d.logger.Errorf("api: failed to delete client: %v", err)
 | 
						|
		return nil, fmt.Errorf("delete client: %v", err)
 | 
						|
	}
 | 
						|
	return &api.DeleteClientResp{}, nil
 | 
						|
}
 | 
						|
 | 
						|
// checkCost returns an error if the hash provided does not meet lower or upper
 | 
						|
// bound cost requirements.
 | 
						|
func checkCost(hash []byte) error {
 | 
						|
	actual, err := bcrypt.Cost(hash)
 | 
						|
	if err != nil {
 | 
						|
		return fmt.Errorf("parsing bcrypt hash: %v", err)
 | 
						|
	}
 | 
						|
	if actual < bcrypt.DefaultCost {
 | 
						|
		return fmt.Errorf("given hash cost = %d does not meet minimum cost requirement = %d", actual, bcrypt.DefaultCost)
 | 
						|
	}
 | 
						|
	if actual > upBoundCost {
 | 
						|
		return fmt.Errorf("given hash cost = %d is above upper bound cost = %d, recommended cost = %d", actual, upBoundCost, recCost)
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (d dexAPI) CreatePassword(ctx context.Context, req *api.CreatePasswordReq) (*api.CreatePasswordResp, error) {
 | 
						|
	if req.Password == nil {
 | 
						|
		return nil, errors.New("no password supplied")
 | 
						|
	}
 | 
						|
	if req.Password.UserId == "" {
 | 
						|
		return nil, errors.New("no user ID supplied")
 | 
						|
	}
 | 
						|
	if req.Password.Hash != nil {
 | 
						|
		if err := checkCost(req.Password.Hash); err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
	} else {
 | 
						|
		return nil, errors.New("no hash of password supplied")
 | 
						|
	}
 | 
						|
 | 
						|
	p := storage.Password{
 | 
						|
		Email:    req.Password.Email,
 | 
						|
		Hash:     req.Password.Hash,
 | 
						|
		Username: req.Password.Username,
 | 
						|
		UserID:   req.Password.UserId,
 | 
						|
	}
 | 
						|
	if err := d.s.CreatePassword(p); err != nil {
 | 
						|
		if err == storage.ErrAlreadyExists {
 | 
						|
			return &api.CreatePasswordResp{AlreadyExists: true}, nil
 | 
						|
		}
 | 
						|
		d.logger.Errorf("api: failed to create password: %v", err)
 | 
						|
		return nil, fmt.Errorf("create password: %v", err)
 | 
						|
	}
 | 
						|
 | 
						|
	return &api.CreatePasswordResp{}, nil
 | 
						|
}
 | 
						|
 | 
						|
func (d dexAPI) UpdatePassword(ctx context.Context, req *api.UpdatePasswordReq) (*api.UpdatePasswordResp, error) {
 | 
						|
	if req.Email == "" {
 | 
						|
		return nil, errors.New("no email supplied")
 | 
						|
	}
 | 
						|
	if req.NewHash == nil && req.NewUsername == "" {
 | 
						|
		return nil, errors.New("nothing to update")
 | 
						|
	}
 | 
						|
 | 
						|
	if req.NewHash != nil {
 | 
						|
		if err := checkCost(req.NewHash); err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	updater := func(old storage.Password) (storage.Password, error) {
 | 
						|
		if req.NewHash != nil {
 | 
						|
			old.Hash = req.NewHash
 | 
						|
		}
 | 
						|
 | 
						|
		if req.NewUsername != "" {
 | 
						|
			old.Username = req.NewUsername
 | 
						|
		}
 | 
						|
 | 
						|
		return old, nil
 | 
						|
	}
 | 
						|
 | 
						|
	if err := d.s.UpdatePassword(req.Email, updater); err != nil {
 | 
						|
		if err == storage.ErrNotFound {
 | 
						|
			return &api.UpdatePasswordResp{NotFound: true}, nil
 | 
						|
		}
 | 
						|
		d.logger.Errorf("api: failed to update password: %v", err)
 | 
						|
		return nil, fmt.Errorf("update password: %v", err)
 | 
						|
	}
 | 
						|
 | 
						|
	return &api.UpdatePasswordResp{}, nil
 | 
						|
}
 | 
						|
 | 
						|
func (d dexAPI) DeletePassword(ctx context.Context, req *api.DeletePasswordReq) (*api.DeletePasswordResp, error) {
 | 
						|
	if req.Email == "" {
 | 
						|
		return nil, errors.New("no email supplied")
 | 
						|
	}
 | 
						|
 | 
						|
	err := d.s.DeletePassword(req.Email)
 | 
						|
	if err != nil {
 | 
						|
		if err == storage.ErrNotFound {
 | 
						|
			return &api.DeletePasswordResp{NotFound: true}, nil
 | 
						|
		}
 | 
						|
		d.logger.Errorf("api: failed to delete password: %v", err)
 | 
						|
		return nil, fmt.Errorf("delete password: %v", err)
 | 
						|
	}
 | 
						|
	return &api.DeletePasswordResp{}, nil
 | 
						|
}
 | 
						|
 | 
						|
func (d dexAPI) GetVersion(ctx context.Context, req *api.VersionReq) (*api.VersionResp, error) {
 | 
						|
	return &api.VersionResp{
 | 
						|
		Server: version.Version,
 | 
						|
		Api:    apiVersion,
 | 
						|
	}, nil
 | 
						|
}
 | 
						|
 | 
						|
func (d dexAPI) ListPasswords(ctx context.Context, req *api.ListPasswordReq) (*api.ListPasswordResp, error) {
 | 
						|
	passwordList, err := d.s.ListPasswords()
 | 
						|
	if err != nil {
 | 
						|
		d.logger.Errorf("api: failed to list passwords: %v", err)
 | 
						|
		return nil, fmt.Errorf("list passwords: %v", err)
 | 
						|
	}
 | 
						|
 | 
						|
	var passwords []*api.Password
 | 
						|
	for _, password := range passwordList {
 | 
						|
		p := api.Password{
 | 
						|
			Email:    password.Email,
 | 
						|
			Username: password.Username,
 | 
						|
			UserId:   password.UserID,
 | 
						|
		}
 | 
						|
		passwords = append(passwords, &p)
 | 
						|
	}
 | 
						|
 | 
						|
	return &api.ListPasswordResp{
 | 
						|
		Passwords: passwords,
 | 
						|
	}, nil
 | 
						|
}
 | 
						|
 | 
						|
func (d dexAPI) VerifyPassword(ctx context.Context, req *api.VerifyPasswordReq) (*api.VerifyPasswordResp, error) {
 | 
						|
	if req.Email == "" {
 | 
						|
		return nil, errors.New("no email supplied")
 | 
						|
	}
 | 
						|
 | 
						|
	if req.Password == "" {
 | 
						|
		return nil, errors.New("no password to verify supplied")
 | 
						|
	}
 | 
						|
 | 
						|
	password, err := d.s.GetPassword(req.Email)
 | 
						|
	if err != nil {
 | 
						|
		if err == storage.ErrNotFound {
 | 
						|
			return &api.VerifyPasswordResp{
 | 
						|
				NotFound: true,
 | 
						|
			}, nil
 | 
						|
		}
 | 
						|
		d.logger.Errorf("api: there was an error retrieving the password: %v", err)
 | 
						|
		return nil, fmt.Errorf("verify password: %v", err)
 | 
						|
	}
 | 
						|
 | 
						|
	if err := bcrypt.CompareHashAndPassword(password.Hash, []byte(req.Password)); err != nil {
 | 
						|
		d.logger.Infof("api: password check failed: %v", err)
 | 
						|
		return &api.VerifyPasswordResp{
 | 
						|
			Verified: false,
 | 
						|
		}, nil
 | 
						|
	}
 | 
						|
	return &api.VerifyPasswordResp{
 | 
						|
		Verified: true,
 | 
						|
	}, nil
 | 
						|
}
 | 
						|
 | 
						|
func (d dexAPI) ListRefresh(ctx context.Context, req *api.ListRefreshReq) (*api.ListRefreshResp, error) {
 | 
						|
	id := new(internal.IDTokenSubject)
 | 
						|
	if err := internal.Unmarshal(req.UserId, id); err != nil {
 | 
						|
		d.logger.Errorf("api: failed to unmarshal ID Token subject: %v", err)
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	var refreshTokenRefs []*api.RefreshTokenRef
 | 
						|
	offlineSessions, err := d.s.GetOfflineSessions(id.UserId, id.ConnId)
 | 
						|
	if err != nil {
 | 
						|
		if err == storage.ErrNotFound {
 | 
						|
			// This means that this user-client pair does not have a refresh token yet.
 | 
						|
			// An empty list should be returned instead of an error.
 | 
						|
			return &api.ListRefreshResp{
 | 
						|
				RefreshTokens: refreshTokenRefs,
 | 
						|
			}, nil
 | 
						|
		}
 | 
						|
		d.logger.Errorf("api: failed to list refresh tokens %t here : %v", err == storage.ErrNotFound, err)
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	for _, session := range offlineSessions.Refresh {
 | 
						|
		r := api.RefreshTokenRef{
 | 
						|
			Id:        session.ID,
 | 
						|
			ClientId:  session.ClientID,
 | 
						|
			CreatedAt: session.CreatedAt.Unix(),
 | 
						|
			LastUsed:  session.LastUsed.Unix(),
 | 
						|
		}
 | 
						|
		refreshTokenRefs = append(refreshTokenRefs, &r)
 | 
						|
	}
 | 
						|
 | 
						|
	return &api.ListRefreshResp{
 | 
						|
		RefreshTokens: refreshTokenRefs,
 | 
						|
	}, nil
 | 
						|
}
 | 
						|
 | 
						|
func (d dexAPI) RevokeRefresh(ctx context.Context, req *api.RevokeRefreshReq) (*api.RevokeRefreshResp, error) {
 | 
						|
	id := new(internal.IDTokenSubject)
 | 
						|
	if err := internal.Unmarshal(req.UserId, id); err != nil {
 | 
						|
		d.logger.Errorf("api: failed to unmarshal ID Token subject: %v", err)
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	var (
 | 
						|
		refreshID string
 | 
						|
		notFound  bool
 | 
						|
	)
 | 
						|
	updater := func(old storage.OfflineSessions) (storage.OfflineSessions, error) {
 | 
						|
		refreshRef := old.Refresh[req.ClientId]
 | 
						|
		if refreshRef == nil || refreshRef.ID == "" {
 | 
						|
			d.logger.Errorf("api: refresh token issued to client %q for user %q not found for deletion", req.ClientId, id.UserId)
 | 
						|
			notFound = true
 | 
						|
			return old, storage.ErrNotFound
 | 
						|
		}
 | 
						|
 | 
						|
		refreshID = refreshRef.ID
 | 
						|
 | 
						|
		// Remove entry from Refresh list of the OfflineSession object.
 | 
						|
		delete(old.Refresh, req.ClientId)
 | 
						|
 | 
						|
		return old, nil
 | 
						|
	}
 | 
						|
 | 
						|
	if err := d.s.UpdateOfflineSessions(id.UserId, id.ConnId, updater); err != nil {
 | 
						|
		if err == storage.ErrNotFound {
 | 
						|
			return &api.RevokeRefreshResp{NotFound: true}, nil
 | 
						|
		}
 | 
						|
		d.logger.Errorf("api: failed to update offline session object: %v", err)
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	if notFound {
 | 
						|
		return &api.RevokeRefreshResp{NotFound: true}, nil
 | 
						|
	}
 | 
						|
 | 
						|
	// Delete the refresh token from the storage
 | 
						|
	//
 | 
						|
	// TODO(ericchiang): we don't have any good recourse if this call fails.
 | 
						|
	// Consider garbage collection of refresh tokens with no associated ref.
 | 
						|
	if err := d.s.DeleteRefresh(refreshID); err != nil {
 | 
						|
		d.logger.Errorf("failed to delete refresh token: %v", err)
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	return &api.RevokeRefreshResp{}, nil
 | 
						|
}
 |