dex/server/config.go
Bobby Rullo b80dbc8975 server: support out-of-band auth flow
When "urn:ietf:wg:oauth:2.0:oob" is used as a redirect URI, redirect to
an internal dex page where the user is shown the code and instructed to
paste it into their app.
2016-06-20 17:03:13 -07:00

402 lines
11 KiB
Go

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/pkg/health"
"github.com/go-gorp/gorp"
"github.com/coreos/dex/client"
clientmanager "github.com/coreos/dex/client/manager"
"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
RegisterOnFirstLogin 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,
RegisterOnFirstLogin: cfg.RegisterOnFirstLogin,
}
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)
}
clientRepo, err := db.NewClientRepoFromClients(dbMap, clients)
if err != nil {
return 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{})
clientManager := clientmanager.NewClientManager(clientRepo, db.TransactionFactory(dbMap), clientmanager.ManagerOptions{})
if err != nil {
return fmt.Errorf("Failed to create client identity manager: %v", err)
}
srv.ClientRepo = clientRepo
srv.ClientManager = clientManager
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))
srv.dbMap = 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"`
// The old format stored all user data under the "user" key.
// Attempt to detect that, and print an better error.
OldUserFields map[string]string `json:"user"`
}
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 {
if u.OldUserFields != nil {
return nil, nil, fmt.Errorf("Static user file is using an outdated format. Please refer to example in static/fixtures.")
}
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 a list of clients.
func loadClients(filepath string) ([]client.LoadableClient, error) {
f, err := os.Open(filepath)
if err != nil {
return nil, err
}
defer f.Close()
return client.ClientsFromReader(f)
}
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.NewClientRepo(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{})
clientManager := clientmanager.NewClientManager(ciRepo, db.TransactionFactory(dbc), clientmanager.ManagerOptions{})
refreshTokenRepo := db.NewRefreshTokenRepo(dbc)
sm := sessionmanager.NewSessionManager(sRepo, skRepo)
srv.ClientRepo = ciRepo
srv.ClientManager = clientManager
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))
srv.dbMap = 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 {
for _, t := range []struct {
templateName string
templatePtr **template.Template
}{
{LoginPageTemplateName, &srv.LoginTemplate},
{RegisterTemplateName, &srv.RegisterTemplate},
{VerifyEmailTemplateName, &srv.VerifyEmailTemplate},
{SendResetPasswordEmailTemplateName, &srv.SendResetPasswordEmailTemplate},
{ResetPasswordTemplateName, &srv.ResetPasswordTemplate},
{OOBTemplateName, &srv.OOBTemplate},
} {
tpl, err := findTemplate(t.templateName, tpls)
if err != nil {
return err
}
*t.templatePtr = tpl
}
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
}