72a431dd4b
Switch from using "text/template" to "html/template", which provides basic XSS preventions. We haven't identified any particular place where unsanitized user data is rendered to the frontend. This is just a preventative step. At the same time, make more templates take pure URL instead of forming an URL themselves using an "authReqID" argument. This will help us stop using the auth req ID in certain places, preventing garbage collection from killing login flows that wait too long at the login screen. Also increase the login session window (time between initial redirect and the user logging in) from 30 minutes to 24 hours, and display a more helpful error message when the session expires. How to test: 1. Spin up dex and example with examples/config-dev.yaml. 2. Login through both the password prompt and the direct redirect. 3. Edit examples/config-dev.yaml removing the "connectors" section. 4. Ensure you can still login with a password. (email/password is "admin@example.com" and "password")
256 lines
6.2 KiB
Go
256 lines
6.2 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) },
|
|
}
|
|
|
|
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 string, lastWasInvalid bool) error {
|
|
data := struct {
|
|
PostURL string
|
|
Username string
|
|
Invalid bool
|
|
}{postURL, lastUsername, 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
|
|
}
|