Commit c59e1aaa authored by ale's avatar ale

Add some template customization options in the config

Support serving site-specific logo, favicon, and setting a site title.
parent 8c093e64
Pipeline #2234 passed with stage
in 24 seconds
......@@ -1118,7 +1118,7 @@ var _templatesLogin_otpHtml = []byte(`{{template "header" .}}
{{end}}
<label for="inputOTP" class="sr-only">OTP</label>
<input type="text" id="inputOTP" name="otp" size="6" class="form-control" required autofocus>
<input type="text" id="inputOTP" name="otp" size="6" class="form-control" required autofocus autocomplete="off">
<button type="submit" class="btn btn-primary">Login</button>
......@@ -1137,7 +1137,7 @@ func templatesLogin_otpHtml() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "templates/login_otp.html", size: 543, mode: os.FileMode(420), modTime: time.Unix(1541234791, 0)}
info := bindataFileInfo{name: "templates/login_otp.html", size: 562, mode: os.FileMode(420), modTime: time.Unix(1550307595, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
......@@ -1164,13 +1164,13 @@ var _templatesLogin_passwordHtml = []byte(`{{template "header" .}}
value="{{.Username}}"
{{else}}
placeholder="Username"
{{end}} required autofocus>
{{end}} autocomplete="off" required autofocus>
<label for="inputPassword" class="sr-only">
Password
</label>
<input type="password" name="password" id="inputPassword"
class="form-control" placeholder="Password" required>
class="form-control" placeholder="Password" autocomplete="off" required>
{{if .AccountRecoveryURL}}
<p>
......@@ -1199,7 +1199,7 @@ func templatesLogin_passwordHtml() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "templates/login_password.html", size: 1149, mode: os.FileMode(420), modTime: time.Unix(1542882702, 0)}
info := bindataFileInfo{name: "templates/login_password.html", size: 1187, mode: os.FileMode(420), modTime: time.Unix(1550307045, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
......@@ -1312,7 +1312,8 @@ var _templatesPageHtml = []byte(`{{define "header"}}<!DOCTYPE html>
{{if .U2FSignRequest}}<meta name="u2f_request" value="{{json .U2FSignRequest}}">{{end}}
<link rel="stylesheet" href="{{.URLPrefix}}/static/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO">
<link rel="stylesheet" href="{{.URLPrefix}}/static/css/signin.css" integrity="sha384-9Y3UkAyM3svAuamEoaXIxe+1MqBKJdZtL8S1FZjvE1XqkICDH7DTXNavnFV8Uk2o">
<title>Sign In</title>
{{if .SiteFavicon}}<link rel="icon" type="image/x-icon" href="{{.URLPrefix}}/favicon.ico">{{end}}
<title>{{if .SiteName}}{{.SiteName}} - {{end}}Sign In</title>
</head>
<body>
......@@ -1347,7 +1348,7 @@ func templatesPageHtml() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "templates/page.html", size: 1729, mode: os.FileMode(420), modTime: time.Unix(1550305837, 0)}
info := bindataFileInfo{name: "templates/page.html", size: 1870, mode: os.FileMode(420), modTime: time.Unix(1550311459, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
......
......@@ -42,6 +42,10 @@ type Config struct {
KeyStoreEnableGroups []string `yaml:"keystore_enable_groups"`
AccountRecoveryURL string `yaml:"account_recovery_url"`
SiteName string `yaml:"site_name"`
SiteLogo string `yaml:"site_logo"`
SiteFavicon string `yaml:"site_favicon"`
allowedServicesRx []*regexp.Regexp
}
......
......@@ -4,15 +4,18 @@ package server
//go:generate go-bindata --nocompress --pkg server static/... templates/...
import (
"bytes"
"context"
"encoding/gob"
"encoding/json"
"fmt"
"html/template"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"strings"
"time"
......@@ -91,9 +94,13 @@ type Server struct {
keystore ksclient.Client
keystoreGroups []string
csrfSecret []byte
tpl *template.Template
renderer *renderer
urlPrefix string
homepageRedirectURL string
// User-configurable static data that we serve from memory.
siteLogo *staticContent
siteFavicon *staticContent
}
func sl2bl(sl []string) [][]byte {
......@@ -116,13 +123,14 @@ func New(loginService *LoginService, authClient authclient.Client, config *Confi
Path: urlPrefix + "/",
}
renderer := newRenderer(config)
s := &Server{
authSessionLifetime: defaultAuthSessionLifetime,
authSessionStore: store,
loginService: loginService,
urlPrefix: urlPrefix,
homepageRedirectURL: config.HomepageRedirectURL,
tpl: parseEmbeddedTemplates(),
renderer: renderer,
}
if config.CSRFSecret != "" {
s.csrfSecret = []byte(config.CSRFSecret)
......@@ -131,6 +139,21 @@ func New(loginService *LoginService, authClient authclient.Client, config *Confi
s.authSessionLifetime = time.Duration(config.AuthSessionLifetimeSeconds) * time.Second
}
if config.SiteLogo != "" {
siteLogo, err := loadStaticContent(config.SiteLogo)
if err != nil {
return nil, err
}
s.siteLogo = siteLogo
}
if config.SiteFavicon != "" {
siteFavicon, err := loadStaticContent(config.SiteFavicon)
if err != nil {
return nil, err
}
s.siteFavicon = siteFavicon
}
if config.KeyStore != nil {
ks, err := ksclient.New(config.KeyStore)
if err != nil {
......@@ -146,8 +169,8 @@ func New(loginService *LoginService, authClient authclient.Client, config *Confi
return nil, err
}
s.loginHandler = newLoginHandler(s.loginCallback, devMgr, authClient,
config.AuthService, config.U2FAppID, config.URLPrefix, config.AccountRecoveryURL,
s.tpl, sessionSecrets...)
config.AuthService, config.U2FAppID, config.URLPrefix,
renderer, sessionSecrets...)
return s, nil
}
......@@ -309,11 +332,9 @@ func (h *Server) handleLogout(w http.ResponseWriter, req *http.Request, session
svcJSON, _ := json.Marshal(svcs) // nolint
data := map[string]interface{}{
"CSRFField": csrf.TemplateField(req),
"URLPrefix": h.urlPrefix,
"Services": svcs,
"IncludeLogoutScripts": true,
"ServicesJSON": string(svcJSON),
"IncludeLogoutScripts": true,
}
// Clear the local session. Ignore errors.
......@@ -335,7 +356,13 @@ func (h *Server) handleLogout(w http.ResponseWriter, req *http.Request, session
w.Header().Set("Content-Security-Policy", logoutContentSecurityPolicy)
h.tpl.ExecuteTemplate(w, "logout.html", data) // nolint
body, err := h.renderer.Render(req, "logout.html", data)
if err != nil {
log.Printf("template error in logout(): %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write(body) // nolint
}
func (h *Server) handleExchange(w http.ResponseWriter, req *http.Request) {
......@@ -376,6 +403,14 @@ func (h *Server) Handler() http.Handler {
// everywhere.
root := mux.NewRouter()
// If we have customized content, serve it from well-known URLs.
if h.siteLogo != nil {
root.Handle(h.urlFor("/img/site_logo"), h.siteLogo)
}
if h.siteFavicon != nil {
root.Handle(h.urlFor("/favicon.ico"), h.siteFavicon)
}
// Serve static content to anyone.
staticPath := h.urlFor("/static/")
root.PathPrefix(staticPath).Handler(http.StripPrefix(staticPath, http.FileServer(&assetfs.AssetFS{
......@@ -458,3 +493,65 @@ func parseEmbeddedTemplates() *template.Template {
}
return root
}
type renderer struct {
tpl *template.Template
urlPrefix string
siteName string
siteLogo string
siteFavicon string
accountRecoveryURL string
}
func newRenderer(config *Config) *renderer {
return &renderer{
tpl: parseEmbeddedTemplates(),
urlPrefix: config.URLPrefix,
accountRecoveryURL: config.AccountRecoveryURL,
siteName: config.SiteName,
siteLogo: config.SiteLogo,
siteFavicon: config.SiteFavicon,
}
}
func (r *renderer) Render(req *http.Request, templateName string, data map[string]interface{}) ([]byte, error) {
data["CSRFField"] = csrf.TemplateField(req)
data["URLPrefix"] = r.urlPrefix
data["AccountRecoveryURL"] = r.accountRecoveryURL
data["SiteName"] = r.siteName
data["SiteLogo"] = r.siteLogo
data["SiteFavicon"] = r.siteFavicon
var buf bytes.Buffer
if err := r.tpl.ExecuteTemplate(&buf, templateName, data); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
type staticContent struct {
modtime time.Time
name string
data []byte
}
func loadStaticContent(path string) (*staticContent, error) {
stat, err := os.Stat(path)
if err != nil {
return nil, err
}
data, err := ioutil.ReadFile(path) // #nosec
if err != nil {
return nil, err
}
return &staticContent{
name: path,
modtime: stat.ModTime(),
data: data,
}, nil
}
func (c *staticContent) ServeHTTP(w http.ResponseWriter, req *http.Request) {
http.ServeContent(w, req, c.name, c.modtime, bytes.NewReader(c.data))
}
package server
import (
"bytes"
"encoding/gob"
"encoding/json"
"errors"
"fmt"
"html/template"
"log"
"net/http"
"net/url"
"strings"
"time"
"github.com/gorilla/csrf"
"github.com/gorilla/sessions"
"github.com/tstranex/u2f"
"go.opencensus.io/trace"
......@@ -72,20 +69,19 @@ func init() {
type loginCallbackFunc func(http.ResponseWriter, *http.Request, string, string, *auth.UserInfo) error
type loginHandler struct {
authClient authclient.Client
authService string
u2fAppID string
urlPrefix string
devMgr *device.Manager
loginCallback loginCallbackFunc
loginSessionStore sessions.Store
tpl *template.Template
accountRecoveryURL string
authClient authclient.Client
authService string
u2fAppID string
urlPrefix string
devMgr *device.Manager
loginCallback loginCallbackFunc
loginSessionStore sessions.Store
renderer *renderer
}
// NewLoginHandler will wrap an http.Handler with the login workflow,
// invoking it only on successful login.
func newLoginHandler(okHandler loginCallbackFunc, devMgr *device.Manager, authClient authclient.Client, authService, u2fAppID, urlPrefix, accountRecoveryURL string, tpl *template.Template, keyPairs ...[]byte) *loginHandler {
func newLoginHandler(okHandler loginCallbackFunc, devMgr *device.Manager, authClient authclient.Client, authService, u2fAppID, urlPrefix string, rndr *renderer, keyPairs ...[]byte) *loginHandler {
store := sessions.NewCookieStore(keyPairs...)
store.Options = &sessions.Options{
HttpOnly: true,
......@@ -93,15 +89,14 @@ func newLoginHandler(okHandler loginCallbackFunc, devMgr *device.Manager, authCl
MaxAge: 0,
}
return &loginHandler{
authClient: authClient,
authService: authService,
u2fAppID: u2fAppID,
urlPrefix: strings.TrimRight(urlPrefix, "/"),
devMgr: devMgr,
loginCallback: okHandler,
loginSessionStore: store,
accountRecoveryURL: accountRecoveryURL,
tpl: parseEmbeddedTemplates(),
authClient: authClient,
authService: authService,
u2fAppID: u2fAppID,
urlPrefix: strings.TrimRight(urlPrefix, "/"),
devMgr: devMgr,
loginCallback: okHandler,
loginSessionStore: store,
renderer: rndr,
}
}
......@@ -245,7 +240,8 @@ func (l *loginHandler) handlePassword(w http.ResponseWriter, req *http.Request,
env["Error"] = true
}
return l.executeTemplateToBuffer(req, "login_password.html", env)
body, err := l.renderer.Render(req, "login_password.html", env)
return loginStateNone, body, err
}
// Handle login with password and TOTP.
......@@ -265,7 +261,8 @@ func (l *loginHandler) handleOTP(w http.ResponseWriter, req *http.Request, sessi
env["Error"] = true
}
return l.executeTemplateToBuffer(req, "login_otp.html", env)
body, err := l.renderer.Render(req, "login_otp.html", env)
return loginStateNone, body, err
}
// Handle login with password and hardware token.
......@@ -293,7 +290,8 @@ func (l *loginHandler) handleU2F(w http.ResponseWriter, req *http.Request, sessi
env["Error"] = true
}
return l.executeTemplateToBuffer(req, "login_u2f.html", env)
body, err := l.renderer.Render(req, "login_u2f.html", env)
return loginStateNone, body, err
}
// Make the auth request to the authentication server.
......@@ -346,19 +344,6 @@ func (l *loginHandler) makeLoginURL(req *http.Request) string {
return fmt.Sprintf("%s/login?%s", l.urlPrefix, v.Encode())
}
// Renders a template to a buffer, with a return value that makes it
// convenient to use in login handlers.
func (l *loginHandler) executeTemplateToBuffer(req *http.Request, templateName string, data map[string]interface{}) (loginState, []byte, error) {
data["CSRFField"] = csrf.TemplateField(req)
data["URLPrefix"] = l.urlPrefix
data["AccountRecoveryURL"] = l.accountRecoveryURL
var buf bytes.Buffer
if err := l.tpl.ExecuteTemplate(&buf, templateName, data); err != nil {
return loginStateNone, nil, err
}
return loginStateNone, buf.Bytes(), nil
}
// Template helper function that encodes its input as JSON.
func toJSON(obj interface{}) string {
data, err := json.Marshal(obj)
......
......@@ -6,7 +6,8 @@
{{if .U2FSignRequest}}<meta name="u2f_request" value="{{json .U2FSignRequest}}">{{end}}
<link rel="stylesheet" href="{{.URLPrefix}}/static/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO">
<link rel="stylesheet" href="{{.URLPrefix}}/static/css/signin.css" integrity="sha384-9Y3UkAyM3svAuamEoaXIxe+1MqBKJdZtL8S1FZjvE1XqkICDH7DTXNavnFV8Uk2o">
<title>Sign In</title>
{{if .SiteFavicon}}<link rel="icon" type="image/x-icon" href="{{.URLPrefix}}/favicon.ico">{{end}}
<title>{{if .SiteName}}{{.SiteName}} - {{end}}Sign In</title>
</head>
<body>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment