41740179af
You can specify your own client ID and secret!
262 lines
6.1 KiB
Go
262 lines
6.1 KiB
Go
package manager
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"net/url"
|
|
|
|
"errors"
|
|
|
|
"github.com/coreos/dex/client"
|
|
pcrypto "github.com/coreos/dex/pkg/crypto"
|
|
"github.com/coreos/dex/pkg/log"
|
|
"github.com/coreos/dex/repo"
|
|
"github.com/coreos/go-oidc/oidc"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
const (
|
|
// Blowfish, the algorithm underlying bcrypt, has a maximum
|
|
// password length of 72. We explicitly track and check this
|
|
// since the bcrypt library will silently ignore portions of
|
|
// a password past the first 72 characters.
|
|
maxSecretLength = 72
|
|
)
|
|
|
|
var (
|
|
localHostRedirectURL = mustParseURL("http://localhost:0")
|
|
)
|
|
|
|
type ClientOptions struct {
|
|
TrustedPeers []string
|
|
}
|
|
|
|
type SecretGenerator func() ([]byte, error)
|
|
|
|
func DefaultSecretGenerator() ([]byte, error) {
|
|
return pcrypto.RandBytes(maxSecretLength)
|
|
}
|
|
|
|
func CompareHashAndPassword(hashedPassword, password []byte) error {
|
|
if len(password) > maxSecretLength {
|
|
return errors.New("password length greater than max secret length")
|
|
}
|
|
return bcrypt.CompareHashAndPassword(hashedPassword, password)
|
|
}
|
|
|
|
// ClientManager performs client-related "business-logic" functions on client and related objects.
|
|
// This is in contrast to the Repos which perform little more than CRUD operations.
|
|
type ClientManager struct {
|
|
clientRepo client.ClientRepo
|
|
begin repo.TransactionFactory
|
|
secretGenerator SecretGenerator
|
|
clientIDGenerator func(string) (string, error)
|
|
}
|
|
|
|
type ManagerOptions struct {
|
|
SecretGenerator func() ([]byte, error)
|
|
ClientIDGenerator func(string) (string, error)
|
|
}
|
|
|
|
func NewClientManager(clientRepo client.ClientRepo, txnFactory repo.TransactionFactory, options ManagerOptions) *ClientManager {
|
|
if options.SecretGenerator == nil {
|
|
options.SecretGenerator = DefaultSecretGenerator
|
|
}
|
|
if options.ClientIDGenerator == nil {
|
|
options.ClientIDGenerator = oidc.GenClientID
|
|
}
|
|
return &ClientManager{
|
|
clientRepo: clientRepo,
|
|
begin: txnFactory,
|
|
secretGenerator: options.SecretGenerator,
|
|
clientIDGenerator: options.ClientIDGenerator,
|
|
}
|
|
}
|
|
|
|
// New creates and persists a new client with the given options, returning the generated credentials.
|
|
// Any Credenials provided with the client are ignored and overwritten by the generated ID and Secret.
|
|
// "Normal" (i.e. non-Public) clients must have at least one valid RedirectURI in their Metadata.
|
|
// Public clients must not have any RedirectURIs and must have a client name.
|
|
func (m *ClientManager) New(cli client.Client, options *ClientOptions) (*oidc.ClientCredentials, error) {
|
|
tx, err := m.begin()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
if err := validateClient(cli); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = m.addClientCredentials(&cli)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
creds := cli.Credentials
|
|
|
|
// Save Client
|
|
_, err = m.clientRepo.New(tx, cli)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if options != nil && len(options.TrustedPeers) > 0 {
|
|
err = m.clientRepo.SetTrustedPeers(tx, creds.ID, options.TrustedPeers)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
err = tx.Commit()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Returns creds with unhashed secret
|
|
return &creds, nil
|
|
}
|
|
|
|
func (m *ClientManager) Get(id string) (client.Client, error) {
|
|
return m.clientRepo.Get(nil, id)
|
|
}
|
|
|
|
func (m *ClientManager) All() ([]client.Client, error) {
|
|
return m.clientRepo.All(nil)
|
|
}
|
|
|
|
func (m *ClientManager) Metadata(clientID string) (*oidc.ClientMetadata, error) {
|
|
c, err := m.clientRepo.Get(nil, clientID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &c.Metadata, nil
|
|
}
|
|
|
|
func (m *ClientManager) IsDexAdmin(clientID string) (bool, error) {
|
|
c, err := m.clientRepo.Get(nil, clientID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return c.Admin, nil
|
|
}
|
|
|
|
func (m *ClientManager) SetDexAdmin(clientID string, isAdmin bool) error {
|
|
tx, err := m.begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
c, err := m.clientRepo.Get(tx, clientID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c.Admin = isAdmin
|
|
err = m.clientRepo.Update(tx, c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = tx.Commit()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *ClientManager) Authenticate(creds oidc.ClientCredentials) (bool, error) {
|
|
clientSecret, err := m.clientRepo.GetSecret(nil, creds.ID)
|
|
if err != nil {
|
|
log.Errorf("error getting secret for client ID: %v: err: %v", creds.ID, err)
|
|
return false, nil
|
|
}
|
|
|
|
if clientSecret == nil {
|
|
log.Errorf("no secret found for client ID: %v", creds.ID)
|
|
return false, nil
|
|
}
|
|
|
|
dec, err := base64.URLEncoding.DecodeString(creds.Secret)
|
|
if err != nil {
|
|
log.Errorf("error Decoding client creds: %v", err)
|
|
return false, nil
|
|
}
|
|
|
|
ok := CompareHashAndPassword(clientSecret, dec) == nil
|
|
return ok, nil
|
|
}
|
|
|
|
func (m *ClientManager) addClientCredentials(cli *client.Client) error {
|
|
var seed string
|
|
if cli.Public {
|
|
seed = cli.Metadata.ClientName
|
|
} else {
|
|
seed = cli.Metadata.RedirectURIs[0].Host
|
|
}
|
|
|
|
var err error
|
|
var clientID string
|
|
if cli.Credentials.ID != "" {
|
|
clientID = cli.Credentials.ID
|
|
} else {
|
|
// Generate Client ID
|
|
clientID, err = m.clientIDGenerator(seed)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
var clientSecret string
|
|
if cli.Credentials.Secret != "" {
|
|
clientSecret = cli.Credentials.Secret
|
|
} else {
|
|
// Generate Secret
|
|
secret, err := m.secretGenerator()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
clientSecret = base64.URLEncoding.EncodeToString(secret)
|
|
}
|
|
|
|
cli.Credentials = oidc.ClientCredentials{
|
|
ID: clientID,
|
|
Secret: clientSecret,
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateClient(cli client.Client) error {
|
|
// NOTE: please be careful changing the errors returned here; they are used
|
|
// downstream (eg. in the admin API) to determine the http errors returned.
|
|
if cli.Public {
|
|
if len(cli.Metadata.RedirectURIs) > 0 {
|
|
return client.ErrorPublicClientRedirectURIs
|
|
}
|
|
if cli.Metadata.ClientName == "" {
|
|
return client.ErrorPublicClientMissingName
|
|
}
|
|
cli.Metadata.RedirectURIs = []url.URL{
|
|
localHostRedirectURL,
|
|
}
|
|
} else {
|
|
if len(cli.Metadata.RedirectURIs) < 1 {
|
|
return client.ErrorMissingRedirectURI
|
|
}
|
|
}
|
|
|
|
err := cli.Metadata.Valid()
|
|
if err != nil {
|
|
return client.ValidationError{Err: err}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func mustParseURL(s string) url.URL {
|
|
u, err := url.Parse(s)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return *u
|
|
}
|