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.
425 lines
11 KiB
425 lines
11 KiB
package server |
|
|
|
import ( |
|
"encoding/json" |
|
"errors" |
|
"fmt" |
|
"html/template" |
|
"io" |
|
"net/url" |
|
"os" |
|
"path/filepath" |
|
texttemplate "text/template" |
|
"time" |
|
|
|
"github.com/coreos/go-oidc/key" |
|
"github.com/coreos/go-oidc/oidc" |
|
"github.com/coreos/pkg/health" |
|
"github.com/go-gorp/gorp" |
|
|
|
"github.com/coreos/dex/connector" |
|
"github.com/coreos/dex/db" |
|
"github.com/coreos/dex/email" |
|
sessionmanager "github.com/coreos/dex/session/manager" |
|
"github.com/coreos/dex/user" |
|
useremail "github.com/coreos/dex/user/email" |
|
usermanager "github.com/coreos/dex/user/manager" |
|
) |
|
|
|
type ServerConfig struct { |
|
IssuerURL string |
|
IssuerName string |
|
IssuerLogoURL string |
|
TemplateDir string |
|
EmailTemplateDirs []string |
|
EmailFromAddress string |
|
EmailerConfigFile string |
|
StateConfig StateConfigurer |
|
EnableRegistration bool |
|
EnableClientRegistration bool |
|
} |
|
|
|
type StateConfigurer interface { |
|
Configure(*Server) error |
|
} |
|
|
|
type SingleServerConfig struct { |
|
ClientsFile string |
|
ConnectorsFile string |
|
UsersFile string |
|
} |
|
|
|
type MultiServerConfig struct { |
|
KeySecrets [][]byte |
|
DatabaseConfig db.Config |
|
UseOldFormat bool |
|
} |
|
|
|
func (cfg *ServerConfig) Server() (*Server, error) { |
|
iu, err := url.Parse(cfg.IssuerURL) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
tpl, err := getTemplates(cfg.IssuerName, cfg.IssuerLogoURL, cfg.EnableRegistration, cfg.TemplateDir) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
km := key.NewPrivateKeyManager() |
|
srv := Server{ |
|
IssuerURL: *iu, |
|
KeyManager: km, |
|
Templates: tpl, |
|
|
|
HealthChecks: []health.Checkable{km}, |
|
Connectors: []connector.Connector{}, |
|
|
|
EnableRegistration: cfg.EnableRegistration, |
|
EnableClientRegistration: cfg.EnableClientRegistration, |
|
} |
|
|
|
err = cfg.StateConfig.Configure(&srv) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
err = setTemplates(&srv, tpl) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
err = setEmailer(&srv, cfg.IssuerName, cfg.EmailFromAddress, cfg.EmailerConfigFile, cfg.EmailTemplateDirs) |
|
if err != nil { |
|
return nil, err |
|
} |
|
return &srv, nil |
|
} |
|
|
|
func (cfg *SingleServerConfig) Configure(srv *Server) error { |
|
k, err := key.GeneratePrivateKey() |
|
if err != nil { |
|
return err |
|
} |
|
|
|
dbMap := db.NewMemDB() |
|
|
|
ks := key.NewPrivateKeySet([]*key.PrivateKey{k}, time.Now().Add(24*time.Hour)) |
|
kRepo := key.NewPrivateKeySetRepo() |
|
if err = kRepo.Set(ks); err != nil { |
|
return err |
|
} |
|
|
|
clients, err := loadClients(cfg.ClientsFile) |
|
if err != nil { |
|
return fmt.Errorf("unable to read clients from file %s: %v", cfg.ClientsFile, err) |
|
} |
|
ciRepo, err := db.NewClientIdentityRepoFromClients(dbMap, clients) |
|
if err != nil { |
|
return fmt.Errorf("failed to create client identity repo: %v", err) |
|
} |
|
|
|
f, err := os.Open(cfg.ConnectorsFile) |
|
if err != nil { |
|
return fmt.Errorf("opening connectors file: %v", err) |
|
} |
|
defer f.Close() |
|
cfgs, err := connector.ReadConfigs(f) |
|
if err != nil { |
|
return fmt.Errorf("decoding connector configs: %v", err) |
|
} |
|
cfgRepo := db.NewConnectorConfigRepo(dbMap) |
|
if err := cfgRepo.Set(cfgs); err != nil { |
|
return fmt.Errorf("failed to set connectors: %v", err) |
|
} |
|
|
|
sRepo := db.NewSessionRepo(dbMap) |
|
skRepo := db.NewSessionKeyRepo(dbMap) |
|
sm := sessionmanager.NewSessionManager(sRepo, skRepo) |
|
|
|
users, pwis, err := loadUsers(cfg.UsersFile) |
|
if err != nil { |
|
return fmt.Errorf("unable to read users from file: %v", err) |
|
} |
|
userRepo, err := db.NewUserRepoFromUsers(dbMap, users) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
pwiRepo, err := db.NewPasswordInfoRepoFromPasswordInfos(dbMap, pwis) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
refTokRepo := db.NewRefreshTokenRepo(dbMap) |
|
|
|
txnFactory := db.TransactionFactory(dbMap) |
|
userManager := usermanager.NewUserManager(userRepo, pwiRepo, cfgRepo, txnFactory, usermanager.ManagerOptions{}) |
|
srv.ClientIdentityRepo = ciRepo |
|
srv.KeySetRepo = kRepo |
|
srv.ConnectorConfigRepo = cfgRepo |
|
srv.UserRepo = userRepo |
|
srv.UserManager = userManager |
|
srv.PasswordInfoRepo = pwiRepo |
|
srv.SessionManager = sm |
|
srv.RefreshTokenRepo = refTokRepo |
|
srv.HealthChecks = append(srv.HealthChecks, db.NewHealthChecker(dbMap)) |
|
return nil |
|
} |
|
|
|
// loadUsers parses the user.json file and returns the users to be created. |
|
func loadUsers(filepath string) ([]user.UserWithRemoteIdentities, []user.PasswordInfo, error) { |
|
f, err := os.Open(filepath) |
|
if err != nil { |
|
return nil, nil, err |
|
} |
|
defer f.Close() |
|
return loadUsersFromReader(f) |
|
} |
|
|
|
func loadUsersFromReader(r io.Reader) (users []user.UserWithRemoteIdentities, pwis []user.PasswordInfo, err error) { |
|
// Encoding used by the user config file. |
|
var configUsers []struct { |
|
user.User |
|
Password string `json:"password"` |
|
RemoteIdentities []user.RemoteIdentity `json:"remoteIdentities"` |
|
} |
|
if err := json.NewDecoder(r).Decode(&configUsers); err != nil { |
|
return nil, nil, err |
|
} |
|
|
|
users = make([]user.UserWithRemoteIdentities, len(configUsers)) |
|
pwis = make([]user.PasswordInfo, len(configUsers)) |
|
|
|
for i, u := range configUsers { |
|
users[i] = user.UserWithRemoteIdentities{ |
|
User: u.User, |
|
RemoteIdentities: u.RemoteIdentities, |
|
} |
|
hashedPassword, err := user.NewPasswordFromPlaintext(u.Password) |
|
if err != nil { |
|
return nil, nil, err |
|
} |
|
pwis[i] = user.PasswordInfo{UserID: u.ID, Password: hashedPassword} |
|
} |
|
return |
|
} |
|
|
|
// loadClients parses the clients.json file and returns the clients to be created. |
|
func loadClients(filepath string) ([]oidc.ClientIdentity, error) { |
|
f, err := os.Open(filepath) |
|
if err != nil { |
|
return nil, err |
|
} |
|
defer f.Close() |
|
return loadClientsFromReader(f) |
|
} |
|
|
|
func loadClientsFromReader(r io.Reader) ([]oidc.ClientIdentity, error) { |
|
var c []struct { |
|
ID string `json:"id"` |
|
Secret string `json:"secret"` |
|
RedirectURLs []string `json:"redirectURLs"` |
|
} |
|
if err := json.NewDecoder(r).Decode(&c); err != nil { |
|
return nil, err |
|
} |
|
clients := make([]oidc.ClientIdentity, len(c)) |
|
for i, client := range c { |
|
redirectURIs := make([]url.URL, len(client.RedirectURLs)) |
|
for j, u := range client.RedirectURLs { |
|
uri, err := url.Parse(u) |
|
if err != nil { |
|
return nil, err |
|
} |
|
redirectURIs[j] = *uri |
|
} |
|
|
|
clients[i] = oidc.ClientIdentity{ |
|
Credentials: oidc.ClientCredentials{ |
|
ID: client.ID, |
|
Secret: client.Secret, |
|
}, |
|
Metadata: oidc.ClientMetadata{ |
|
RedirectURIs: redirectURIs, |
|
}, |
|
} |
|
} |
|
return clients, nil |
|
} |
|
|
|
func (cfg *MultiServerConfig) Configure(srv *Server) error { |
|
if len(cfg.KeySecrets) == 0 { |
|
return errors.New("missing key secret") |
|
} |
|
|
|
if cfg.DatabaseConfig.DSN == "" { |
|
return errors.New("missing database connection string") |
|
} |
|
|
|
dbc, err := db.NewConnection(cfg.DatabaseConfig) |
|
if err != nil { |
|
return fmt.Errorf("unable to initialize database connection: %v", err) |
|
} |
|
if _, ok := dbc.Dialect.(gorp.PostgresDialect); !ok { |
|
return errors.New("only postgres backend supported for multi server configurations") |
|
} |
|
|
|
kRepo, err := db.NewPrivateKeySetRepo(dbc, cfg.UseOldFormat, cfg.KeySecrets...) |
|
if err != nil { |
|
return fmt.Errorf("unable to create PrivateKeySetRepo: %v", err) |
|
} |
|
|
|
ciRepo := db.NewClientIdentityRepo(dbc) |
|
sRepo := db.NewSessionRepo(dbc) |
|
skRepo := db.NewSessionKeyRepo(dbc) |
|
cfgRepo := db.NewConnectorConfigRepo(dbc) |
|
userRepo := db.NewUserRepo(dbc) |
|
pwiRepo := db.NewPasswordInfoRepo(dbc) |
|
userManager := usermanager.NewUserManager(userRepo, pwiRepo, cfgRepo, db.TransactionFactory(dbc), usermanager.ManagerOptions{}) |
|
refreshTokenRepo := db.NewRefreshTokenRepo(dbc) |
|
|
|
sm := sessionmanager.NewSessionManager(sRepo, skRepo) |
|
|
|
srv.ClientIdentityRepo = ciRepo |
|
srv.KeySetRepo = kRepo |
|
srv.ConnectorConfigRepo = cfgRepo |
|
srv.UserRepo = userRepo |
|
srv.UserManager = userManager |
|
srv.PasswordInfoRepo = pwiRepo |
|
srv.SessionManager = sm |
|
srv.RefreshTokenRepo = refreshTokenRepo |
|
srv.HealthChecks = append(srv.HealthChecks, db.NewHealthChecker(dbc)) |
|
return nil |
|
} |
|
|
|
func getTemplates(issuerName, issuerLogoURL string, |
|
enableRegister bool, dir string) (*template.Template, error) { |
|
tpl := template.New("").Funcs(map[string]interface{}{ |
|
"issuerName": func() string { |
|
return issuerName |
|
}, |
|
"issuerLogoURL": func() string { |
|
return issuerLogoURL |
|
}, |
|
"enableRegister": func() bool { |
|
return enableRegister |
|
}, |
|
}) |
|
|
|
return tpl.ParseGlob(dir + "/*.html") |
|
} |
|
|
|
func setTemplates(srv *Server, tpls *template.Template) error { |
|
ltpl, err := findTemplate(LoginPageTemplateName, tpls) |
|
if err != nil { |
|
return err |
|
} |
|
srv.LoginTemplate = ltpl |
|
|
|
rtpl, err := findTemplate(RegisterTemplateName, tpls) |
|
if err != nil { |
|
return err |
|
} |
|
srv.RegisterTemplate = rtpl |
|
|
|
vtpl, err := findTemplate(VerifyEmailTemplateName, tpls) |
|
if err != nil { |
|
return err |
|
} |
|
srv.VerifyEmailTemplate = vtpl |
|
|
|
srtpl, err := findTemplate(SendResetPasswordEmailTemplateName, tpls) |
|
if err != nil { |
|
return err |
|
} |
|
srv.SendResetPasswordEmailTemplate = srtpl |
|
|
|
rpwtpl, err := findTemplate(ResetPasswordTemplateName, tpls) |
|
if err != nil { |
|
return err |
|
} |
|
srv.ResetPasswordTemplate = rpwtpl |
|
|
|
return nil |
|
} |
|
|
|
func setEmailer(srv *Server, issuerName, fromAddress, emailerConfigFile string, emailTemplateDirs []string) error { |
|
|
|
cfg, err := email.NewEmailerConfigFromFile(emailerConfigFile) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
emailer, err := cfg.Emailer() |
|
if err != nil { |
|
return err |
|
} |
|
|
|
getFileNames := func(dir, ext string) ([]string, error) { |
|
fns, err := filepath.Glob(dir + "/*." + ext) |
|
if err != nil { |
|
return nil, err |
|
} |
|
return fns, nil |
|
} |
|
getTextFiles := func(dir string) ([]string, error) { |
|
return getFileNames(dir, "txt") |
|
} |
|
getHTMLFiles := func(dir string) ([]string, error) { |
|
return getFileNames(dir, "html") |
|
} |
|
|
|
textTemplates := texttemplate.New("textTemplates") |
|
htmlTemplates := template.New("htmlTemplates") |
|
for _, dir := range emailTemplateDirs { |
|
textFileNames, err := getTextFiles(dir) |
|
if err != nil { |
|
return err |
|
} |
|
if len(textFileNames) != 0 { |
|
textTemplates, err = textTemplates.ParseFiles(textFileNames...) |
|
} |
|
if err != nil { |
|
return err |
|
} |
|
|
|
htmlFileNames, err := getHTMLFiles(dir) |
|
if err != nil { |
|
return err |
|
} |
|
if len(htmlFileNames) != 0 { |
|
htmlTemplates, err = htmlTemplates.ParseFiles(htmlFileNames...) |
|
} |
|
if err != nil { |
|
return err |
|
} |
|
} |
|
tMailer := email.NewTemplatizedEmailerFromTemplates(textTemplates, htmlTemplates, emailer) |
|
tMailer.SetGlobalContext(map[string]interface{}{ |
|
"issuer_name": issuerName, |
|
}) |
|
|
|
ue := useremail.NewUserEmailer(srv.UserRepo, |
|
srv.PasswordInfoRepo, |
|
srv.KeyManager.Signer, |
|
srv.SessionManager.ValidityWindow, |
|
srv.IssuerURL, |
|
tMailer, |
|
fromAddress, |
|
srv.absURL(httpPathResetPassword), |
|
srv.absURL(httpPathEmailVerify), |
|
srv.absURL(httpPathAcceptInvitation), |
|
) |
|
|
|
srv.UserEmailer = ue |
|
return nil |
|
} |
|
|
|
func findTemplate(name string, tpls *template.Template) (*template.Template, error) { |
|
tpl := tpls.Lookup(name) |
|
if tpl == nil { |
|
return nil, fmt.Errorf("unable to find template: %q", name) |
|
} |
|
return tpl, nil |
|
}
|
|
|