mirror of https://github.com/dexidp/dex.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
370 lines
10 KiB
370 lines
10 KiB
package server |
|
|
|
import ( |
|
"context" |
|
"errors" |
|
"fmt" |
|
|
|
"golang.org/x/crypto/bcrypt" |
|
|
|
"github.com/dexidp/dex/api" |
|
"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 |
|
}
|
|
|