2016-10-04 07:26:04 +00:00
|
|
|
package server
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
2016-10-27 23:28:11 +00:00
|
|
|
"fmt"
|
2016-10-04 07:26:04 +00:00
|
|
|
|
2016-10-27 23:28:11 +00:00
|
|
|
"golang.org/x/crypto/bcrypt"
|
2017-03-08 18:33:19 +00:00
|
|
|
|
|
|
|
// go-grpc doesn't use the standard library's context.
|
|
|
|
// https://github.com/grpc/grpc-go/issues/711
|
2016-10-04 07:26:04 +00:00
|
|
|
"golang.org/x/net/context"
|
|
|
|
|
|
|
|
"github.com/coreos/dex/api"
|
2017-02-10 19:33:54 +00:00
|
|
|
"github.com/coreos/dex/server/internal"
|
2016-10-04 07:26:04 +00:00
|
|
|
"github.com/coreos/dex/storage"
|
2016-11-09 20:33:33 +00:00
|
|
|
"github.com/coreos/dex/version"
|
2017-07-25 20:45:17 +00:00
|
|
|
"github.com/sirupsen/logrus"
|
2016-10-04 07:26:04 +00:00
|
|
|
)
|
|
|
|
|
2016-12-13 20:23:16 +00:00
|
|
|
// apiVersion increases every time a new call is added to the API. Clients should use this info
|
2016-11-09 20:33:33 +00:00
|
|
|
// to determine if the server supports specific features.
|
2017-03-17 22:01:21 +00:00
|
|
|
const apiVersion = 2
|
2016-11-09 20:33:33 +00:00
|
|
|
|
2017-07-25 21:26:47 +00:00
|
|
|
// recCost is the recommended bcrypt cost, which balances hash strength and time
|
|
|
|
const recCost = 12
|
|
|
|
|
2016-10-04 07:26:04 +00:00
|
|
|
// NewAPI returns a server which implements the gRPC API interface.
|
2016-12-12 22:54:01 +00:00
|
|
|
func NewAPI(s storage.Storage, logger logrus.FieldLogger) api.DexServer {
|
|
|
|
return dexAPI{
|
|
|
|
s: s,
|
|
|
|
logger: logger,
|
|
|
|
}
|
2016-10-04 07:26:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type dexAPI struct {
|
2016-12-12 22:54:01 +00:00
|
|
|
s storage.Storage
|
|
|
|
logger logrus.FieldLogger
|
2016-10-04 07:26:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2017-02-21 23:00:22 +00:00
|
|
|
if err == storage.ErrAlreadyExists {
|
|
|
|
return &api.CreateClientResp{AlreadyExists: true}, nil
|
|
|
|
}
|
2016-12-12 22:54:01 +00:00
|
|
|
d.logger.Errorf("api: failed to create client: %v", err)
|
2016-10-27 23:28:11 +00:00
|
|
|
return nil, fmt.Errorf("create client: %v", err)
|
2016-10-04 07:26:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return &api.CreateClientResp{
|
|
|
|
Client: req.Client,
|
|
|
|
}, 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
|
|
|
|
}
|
2016-12-12 22:54:01 +00:00
|
|
|
d.logger.Errorf("api: failed to delete client: %v", err)
|
2016-10-27 23:28:11 +00:00
|
|
|
return nil, fmt.Errorf("delete client: %v", err)
|
2016-10-04 07:26:04 +00:00
|
|
|
}
|
|
|
|
return &api.DeleteClientResp{}, nil
|
|
|
|
}
|
2016-10-27 23:28:11 +00:00
|
|
|
|
2017-07-25 21:26:47 +00:00
|
|
|
// checkCost returns an error if the hash provided does not meet minimum cost requirement, and the actual bcrypt cost
|
|
|
|
func checkCost(hash []byte) (int, error) {
|
2016-10-27 23:28:11 +00:00
|
|
|
actual, err := bcrypt.Cost(hash)
|
|
|
|
if err != nil {
|
2017-07-25 21:26:47 +00:00
|
|
|
return 0, fmt.Errorf("parsing bcrypt hash: %v", err)
|
2016-10-27 23:28:11 +00:00
|
|
|
}
|
|
|
|
if actual < bcrypt.DefaultCost {
|
2017-07-25 21:26:47 +00:00
|
|
|
return actual, fmt.Errorf("given hash cost = %d, does not meet minimum cost requirement = %d", actual, bcrypt.DefaultCost)
|
2016-10-27 23:28:11 +00:00
|
|
|
}
|
2017-07-25 21:26:47 +00:00
|
|
|
return actual, nil
|
2016-10-27 23:28:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2017-07-25 21:26:47 +00:00
|
|
|
cost, err := checkCost(req.Password.Hash)
|
|
|
|
if err != nil {
|
2016-10-27 23:28:11 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2017-07-25 21:26:47 +00:00
|
|
|
if cost > recCost {
|
|
|
|
d.logger.Warnln("bcrypt cost = %d, password encryption might timeout. Recommended bcrypt cost is 12", cost)
|
|
|
|
}
|
2016-10-27 23:28:11 +00:00
|
|
|
} 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 {
|
2017-02-21 23:00:22 +00:00
|
|
|
if err == storage.ErrAlreadyExists {
|
|
|
|
return &api.CreatePasswordResp{AlreadyExists: true}, nil
|
|
|
|
}
|
2016-12-12 22:54:01 +00:00
|
|
|
d.logger.Errorf("api: failed to create password: %v", err)
|
2016-10-27 23:28:11 +00:00
|
|
|
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 {
|
2017-07-25 21:26:47 +00:00
|
|
|
cost, err := checkCost(req.NewHash)
|
|
|
|
if err != nil {
|
2016-10-27 23:28:11 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2017-07-25 21:26:47 +00:00
|
|
|
if cost > recCost {
|
|
|
|
d.logger.Warnln("bcrypt cost = %d, password encryption might timeout. Recommended bcrypt cost is 12", cost)
|
|
|
|
}
|
2016-10-27 23:28:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
2016-12-12 22:54:01 +00:00
|
|
|
d.logger.Errorf("api: failed to update password: %v", err)
|
2016-10-27 23:28:11 +00:00
|
|
|
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
|
|
|
|
}
|
2016-12-12 22:54:01 +00:00
|
|
|
d.logger.Errorf("api: failed to delete password: %v", err)
|
2016-10-27 23:28:11 +00:00
|
|
|
return nil, fmt.Errorf("delete password: %v", err)
|
|
|
|
}
|
|
|
|
return &api.DeletePasswordResp{}, nil
|
|
|
|
|
|
|
|
}
|
2016-11-09 20:33:33 +00:00
|
|
|
|
|
|
|
func (d dexAPI) GetVersion(ctx context.Context, req *api.VersionReq) (*api.VersionResp, error) {
|
|
|
|
return &api.VersionResp{
|
|
|
|
Server: version.Version,
|
|
|
|
Api: apiVersion,
|
|
|
|
}, nil
|
|
|
|
}
|
2016-11-17 22:50:58 +00:00
|
|
|
|
|
|
|
func (d dexAPI) ListPasswords(ctx context.Context, req *api.ListPasswordReq) (*api.ListPasswordResp, error) {
|
|
|
|
passwordList, err := d.s.ListPasswords()
|
|
|
|
if err != nil {
|
2016-12-12 22:54:01 +00:00
|
|
|
d.logger.Errorf("api: failed to list passwords: %v", err)
|
2016-11-17 22:50:58 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
}
|
2017-02-10 19:33:54 +00:00
|
|
|
|
|
|
|
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)
|
2017-02-14 22:40:20 +00:00
|
|
|
return nil, err
|
2017-02-10 19:33:54 +00:00
|
|
|
}
|
|
|
|
|
2017-04-28 18:54:39 +00:00
|
|
|
var refreshTokenRefs []*api.RefreshTokenRef
|
2017-02-10 19:33:54 +00:00
|
|
|
offlineSessions, err := d.s.GetOfflineSessions(id.UserId, id.ConnId)
|
|
|
|
if err != nil {
|
2017-04-28 18:54:39 +00:00
|
|
|
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)
|
2017-02-14 22:40:20 +00:00
|
|
|
return nil, err
|
2017-02-10 19:33:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, session := range offlineSessions.Refresh {
|
|
|
|
r := api.RefreshTokenRef{
|
|
|
|
Id: session.ID,
|
|
|
|
ClientId: session.ClientID,
|
2017-03-17 22:01:21 +00:00
|
|
|
CreatedAt: session.CreatedAt.Unix(),
|
|
|
|
LastUsed: session.LastUsed.Unix(),
|
2017-02-10 19:33:54 +00:00
|
|
|
}
|
|
|
|
refreshTokenRefs = append(refreshTokenRefs, &r)
|
|
|
|
}
|
|
|
|
|
|
|
|
return &api.ListRefreshResp{
|
|
|
|
RefreshTokens: refreshTokenRefs,
|
|
|
|
}, nil
|
|
|
|
}
|
2017-02-14 22:40:20 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
updater := func(old storage.OfflineSessions) (storage.OfflineSessions, error) {
|
|
|
|
if refreshID = old.Refresh[req.ClientId].ID; refreshID == "" {
|
|
|
|
return old, fmt.Errorf("user does not have a refresh token for the client = %s", req.ClientId)
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
|
|
|
// Delete the refresh token from the storage
|
|
|
|
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
|
|
|
|
}
|