forked from mystiq/dex
b09a13458f
This allows users of the LDAP connector to give users of Dex' login prompt an idea of what they should enter for a username. Before, irregardless of how the LDAP connector was set up, the prompt was Username [_________________] Password [_________________] Now, this is configurable, and can be used to say "MyCorp SSO Login" if that's what it is. If it's not configured, it will default to "Username". For the passwordDB connector (local users), it is set to "Email Address", since this is what it uses. Signed-off-by: Stephan Renatus <srenatus@chef.io>
257 lines
6.3 KiB
Go
257 lines
6.3 KiB
Go
package server
|
|
|
|
import (
|
|
"fmt"
|
|
"html/template"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
tmplApproval = "approval.html"
|
|
tmplLogin = "login.html"
|
|
tmplPassword = "password.html"
|
|
tmplOOB = "oob.html"
|
|
tmplError = "error.html"
|
|
)
|
|
|
|
var requiredTmpls = []string{
|
|
tmplApproval,
|
|
tmplLogin,
|
|
tmplPassword,
|
|
tmplOOB,
|
|
tmplError,
|
|
}
|
|
|
|
type templates struct {
|
|
loginTmpl *template.Template
|
|
approvalTmpl *template.Template
|
|
passwordTmpl *template.Template
|
|
oobTmpl *template.Template
|
|
errorTmpl *template.Template
|
|
}
|
|
|
|
type webConfig struct {
|
|
dir string
|
|
logoURL string
|
|
issuer string
|
|
theme string
|
|
issuerURL string
|
|
}
|
|
|
|
func join(base, path string) string {
|
|
b := strings.HasSuffix(base, "/")
|
|
p := strings.HasPrefix(path, "/")
|
|
switch {
|
|
case b && p:
|
|
return base + path[1:]
|
|
case b || p:
|
|
return base + path
|
|
default:
|
|
return base + "/" + path
|
|
}
|
|
}
|
|
|
|
func dirExists(dir string) error {
|
|
stat, err := os.Stat(dir)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return fmt.Errorf("directory %q does not exist", dir)
|
|
}
|
|
return fmt.Errorf("stat directory %q: %v", dir, err)
|
|
}
|
|
if !stat.IsDir() {
|
|
return fmt.Errorf("path %q is a file not a directory", dir)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// loadWebConfig returns static assets, theme assets, and templates used by the frontend by
|
|
// reading the directory specified in the webConfig.
|
|
//
|
|
// The directory layout is expected to be:
|
|
//
|
|
// ( web directory )
|
|
// |- static
|
|
// |- themes
|
|
// | |- (theme name)
|
|
// |- templates
|
|
//
|
|
func loadWebConfig(c webConfig) (static, theme http.Handler, templates *templates, err error) {
|
|
if c.theme == "" {
|
|
c.theme = "coreos"
|
|
}
|
|
if c.issuer == "" {
|
|
c.issuer = "dex"
|
|
}
|
|
if c.dir == "" {
|
|
c.dir = "./web"
|
|
}
|
|
if c.logoURL == "" {
|
|
c.logoURL = join(c.issuerURL, "theme/logo.png")
|
|
}
|
|
|
|
if err := dirExists(c.dir); err != nil {
|
|
return nil, nil, nil, fmt.Errorf("load web dir: %v", err)
|
|
}
|
|
|
|
staticDir := filepath.Join(c.dir, "static")
|
|
templatesDir := filepath.Join(c.dir, "templates")
|
|
themeDir := filepath.Join(c.dir, "themes", c.theme)
|
|
|
|
for _, dir := range []string{staticDir, templatesDir, themeDir} {
|
|
if err := dirExists(dir); err != nil {
|
|
return nil, nil, nil, fmt.Errorf("load dir: %v", err)
|
|
}
|
|
}
|
|
|
|
static = http.FileServer(http.Dir(staticDir))
|
|
theme = http.FileServer(http.Dir(themeDir))
|
|
|
|
templates, err = loadTemplates(c, templatesDir)
|
|
return
|
|
}
|
|
|
|
// loadTemplates parses the expected templates from the provided directory.
|
|
func loadTemplates(c webConfig, templatesDir string) (*templates, error) {
|
|
files, err := ioutil.ReadDir(templatesDir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read dir: %v", err)
|
|
}
|
|
|
|
filenames := []string{}
|
|
for _, file := range files {
|
|
if file.IsDir() {
|
|
continue
|
|
}
|
|
filenames = append(filenames, filepath.Join(templatesDir, file.Name()))
|
|
}
|
|
if len(filenames) == 0 {
|
|
return nil, fmt.Errorf("no files in template dir %q", templatesDir)
|
|
}
|
|
|
|
funcs := map[string]interface{}{
|
|
"issuer": func() string { return c.issuer },
|
|
"logo": func() string { return c.logoURL },
|
|
"url": func(s string) string { return join(c.issuerURL, s) },
|
|
"lower": strings.ToLower,
|
|
}
|
|
|
|
tmpls, err := template.New("").Funcs(funcs).ParseFiles(filenames...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse files: %v", err)
|
|
}
|
|
missingTmpls := []string{}
|
|
for _, tmplName := range requiredTmpls {
|
|
if tmpls.Lookup(tmplName) == nil {
|
|
missingTmpls = append(missingTmpls, tmplName)
|
|
}
|
|
}
|
|
if len(missingTmpls) > 0 {
|
|
return nil, fmt.Errorf("missing template(s): %s", missingTmpls)
|
|
}
|
|
return &templates{
|
|
loginTmpl: tmpls.Lookup(tmplLogin),
|
|
approvalTmpl: tmpls.Lookup(tmplApproval),
|
|
passwordTmpl: tmpls.Lookup(tmplPassword),
|
|
oobTmpl: tmpls.Lookup(tmplOOB),
|
|
errorTmpl: tmpls.Lookup(tmplError),
|
|
}, nil
|
|
}
|
|
|
|
var scopeDescriptions = map[string]string{
|
|
"offline_access": "Have offline access",
|
|
"profile": "View basic profile information",
|
|
"email": "View your email",
|
|
}
|
|
|
|
type connectorInfo struct {
|
|
ID string
|
|
Name string
|
|
URL string
|
|
}
|
|
|
|
type byName []connectorInfo
|
|
|
|
func (n byName) Len() int { return len(n) }
|
|
func (n byName) Less(i, j int) bool { return n[i].Name < n[j].Name }
|
|
func (n byName) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
|
|
|
|
func (t *templates) login(w http.ResponseWriter, connectors []connectorInfo) error {
|
|
sort.Sort(byName(connectors))
|
|
data := struct {
|
|
Connectors []connectorInfo
|
|
}{connectors}
|
|
return renderTemplate(w, t.loginTmpl, data)
|
|
}
|
|
|
|
func (t *templates) password(w http.ResponseWriter, postURL, lastUsername, usernamePrompt string, lastWasInvalid bool) error {
|
|
data := struct {
|
|
PostURL string
|
|
Username string
|
|
UsernamePrompt string
|
|
Invalid bool
|
|
}{postURL, lastUsername, usernamePrompt, lastWasInvalid}
|
|
return renderTemplate(w, t.passwordTmpl, data)
|
|
}
|
|
|
|
func (t *templates) approval(w http.ResponseWriter, authReqID, username, clientName string, scopes []string) error {
|
|
accesses := []string{}
|
|
for _, scope := range scopes {
|
|
access, ok := scopeDescriptions[scope]
|
|
if ok {
|
|
accesses = append(accesses, access)
|
|
}
|
|
}
|
|
sort.Strings(accesses)
|
|
data := struct {
|
|
User string
|
|
Client string
|
|
AuthReqID string
|
|
Scopes []string
|
|
}{username, clientName, authReqID, accesses}
|
|
return renderTemplate(w, t.approvalTmpl, data)
|
|
}
|
|
|
|
func (t *templates) oob(w http.ResponseWriter, code string) error {
|
|
data := struct {
|
|
Code string
|
|
}{code}
|
|
return renderTemplate(w, t.oobTmpl, data)
|
|
}
|
|
|
|
func (t *templates) err(w http.ResponseWriter, errType string, errMsg string) error {
|
|
data := struct {
|
|
ErrType string
|
|
ErrMsg string
|
|
}{errType, errMsg}
|
|
return renderTemplate(w, t.errorTmpl, data)
|
|
}
|
|
|
|
// small io.Writer utility to determine if executing the template wrote to the underlying response writer.
|
|
type writeRecorder struct {
|
|
wrote bool
|
|
w io.Writer
|
|
}
|
|
|
|
func (w *writeRecorder) Write(p []byte) (n int, err error) {
|
|
w.wrote = true
|
|
return w.w.Write(p)
|
|
}
|
|
|
|
func renderTemplate(w http.ResponseWriter, tmpl *template.Template, data interface{}) error {
|
|
wr := &writeRecorder{w: w}
|
|
if err := tmpl.Execute(wr, data); err != nil {
|
|
if !wr.wrote {
|
|
// TODO(ericchiang): replace with better internal server error.
|
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
}
|
|
return fmt.Errorf("Error rendering template %s: %s", tmpl.Name(), err)
|
|
}
|
|
return nil
|
|
}
|