forked from mystiq/dex
7c2289e0de
"state" means something specific to OAuth2 and SAML so we don't want to confuse developers who are working on this. Also don't use "session" which could easily be confused with HTTP cookies.
212 lines
5.2 KiB
Go
212 lines
5.2 KiB
Go
package server
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/http"
|
|
"path/filepath"
|
|
"sort"
|
|
"text/template"
|
|
)
|
|
|
|
const (
|
|
tmplApproval = "approval.html"
|
|
tmplLogin = "login.html"
|
|
tmplPassword = "password.html"
|
|
tmplOOB = "oob.html"
|
|
)
|
|
|
|
const coreOSLogoURL = "https://coreos.com/assets/images/brand/coreos-wordmark-135x40px.png"
|
|
|
|
var requiredTmpls = []string{
|
|
tmplApproval,
|
|
tmplLogin,
|
|
tmplPassword,
|
|
tmplOOB,
|
|
}
|
|
|
|
// TemplateConfig describes.
|
|
type TemplateConfig struct {
|
|
// TODO(ericchiang): Asking for a directory with a set of templates doesn't indicate
|
|
// what the templates should look like and doesn't allow consumers of this package to
|
|
// provide their own templates in memory. In the future clean this up.
|
|
|
|
// Directory of the templates. If empty, these will be loaded from memory.
|
|
Dir string `yaml:"dir"`
|
|
|
|
// Defaults to the CoreOS logo and "dex".
|
|
LogoURL string `yaml:"logoURL"`
|
|
Issuer string `yaml:"issuerName"`
|
|
}
|
|
|
|
type globalData struct {
|
|
LogoURL string
|
|
Issuer string
|
|
}
|
|
|
|
func loadTemplates(config TemplateConfig) (*templates, error) {
|
|
var tmpls *template.Template
|
|
if config.Dir != "" {
|
|
files, err := ioutil.ReadDir(config.Dir)
|
|
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(config.Dir, file.Name()))
|
|
}
|
|
if len(filenames) == 0 {
|
|
return nil, fmt.Errorf("no files in template dir %s", config.Dir)
|
|
}
|
|
if tmpls, err = template.ParseFiles(filenames...); err != nil {
|
|
return nil, fmt.Errorf("parse files: %v", err)
|
|
}
|
|
} else {
|
|
// Load templates from memory. This code is largely copied from the standard library's
|
|
// ParseFiles source code.
|
|
// See: https://goo.gl/6Wm4mN
|
|
for name, data := range defaultTemplates {
|
|
var t *template.Template
|
|
if tmpls == nil {
|
|
tmpls = template.New(name)
|
|
}
|
|
if name == tmpls.Name() {
|
|
t = tmpls
|
|
} else {
|
|
t = tmpls.New(name)
|
|
}
|
|
if _, err := t.Parse(data); err != nil {
|
|
return nil, fmt.Errorf("parsing %s: %v", name, 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)
|
|
}
|
|
|
|
if config.LogoURL == "" {
|
|
config.LogoURL = coreOSLogoURL
|
|
}
|
|
if config.Issuer == "" {
|
|
config.Issuer = "dex"
|
|
}
|
|
|
|
return &templates{
|
|
globalData: config,
|
|
loginTmpl: tmpls.Lookup(tmplLogin),
|
|
approvalTmpl: tmpls.Lookup(tmplApproval),
|
|
passwordTmpl: tmpls.Lookup(tmplPassword),
|
|
oobTmpl: tmpls.Lookup(tmplOOB),
|
|
}, nil
|
|
}
|
|
|
|
var scopeDescriptions = map[string]string{
|
|
"offline_access": "Have offline access",
|
|
"profile": "View basic profile information",
|
|
"email": "View your email",
|
|
}
|
|
|
|
type templates struct {
|
|
globalData TemplateConfig
|
|
loginTmpl *template.Template
|
|
approvalTmpl *template.Template
|
|
passwordTmpl *template.Template
|
|
oobTmpl *template.Template
|
|
}
|
|
|
|
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, authReqID string) {
|
|
sort.Sort(byName(connectors))
|
|
|
|
data := struct {
|
|
TemplateConfig
|
|
Connectors []connectorInfo
|
|
AuthReqID string
|
|
}{t.globalData, connectors, authReqID}
|
|
renderTemplate(w, t.loginTmpl, data)
|
|
}
|
|
|
|
func (t *templates) password(w http.ResponseWriter, authReqID, callback, lastUsername string, lastWasInvalid bool) {
|
|
data := struct {
|
|
TemplateConfig
|
|
AuthReqID string
|
|
PostURL string
|
|
Username string
|
|
Invalid bool
|
|
}{t.globalData, authReqID, callback, lastUsername, lastWasInvalid}
|
|
renderTemplate(w, t.passwordTmpl, data)
|
|
}
|
|
|
|
func (t *templates) approval(w http.ResponseWriter, authReqID, username, clientName string, scopes []string) {
|
|
accesses := []string{}
|
|
for _, scope := range scopes {
|
|
access, ok := scopeDescriptions[scope]
|
|
if ok {
|
|
accesses = append(accesses, access)
|
|
}
|
|
}
|
|
sort.Strings(accesses)
|
|
data := struct {
|
|
TemplateConfig
|
|
User string
|
|
Client string
|
|
AuthReqID string
|
|
Scopes []string
|
|
}{t.globalData, username, clientName, authReqID, accesses}
|
|
renderTemplate(w, t.approvalTmpl, data)
|
|
}
|
|
|
|
func (t *templates) oob(w http.ResponseWriter, code string) {
|
|
data := struct {
|
|
TemplateConfig
|
|
Code string
|
|
}{t.globalData, code}
|
|
renderTemplate(w, t.oobTmpl, data)
|
|
}
|
|
|
|
// small io.Writer utilitiy 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{}) {
|
|
wr := &writeRecorder{w: w}
|
|
if err := tmpl.Execute(wr, data); err != nil {
|
|
log.Printf("Error rendering template %s: %s", tmpl.Name(), err)
|
|
|
|
if !wr.wrote {
|
|
// TODO(ericchiang): replace with better internal server error.
|
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
}
|
|
}
|
|
return
|
|
}
|