diff --git a/server/bindata.go b/server/bindata.go index 1a36254f54f064ff15a1bbc5b1fd5aa86edf3ed7..d383806854ce92d3b10db3f023d61131e477f0d2 100644 --- a/server/bindata.go +++ b/server/bindata.go @@ -10,6 +10,7 @@ // templates/login_otp.html // templates/login_password.html // templates/login_u2f.html +// templates/logout.html // templates/page.html // DO NOT EDIT! @@ -72,13 +73,13 @@ func staticCssBootstrapMinCss() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "static/css/bootstrap.min.css", size: 124962, mode: os.FileMode(420), modTime: time.Unix(1509120975, 0)} + info := bindataFileInfo{name: "static/css/bootstrap.min.css", size: 124962, mode: os.FileMode(436), modTime: time.Unix(1510996183, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _staticCssSigninCss = []byte(`body { - padding-top: 20%; + padding-top: 15%; padding-bottom: 20%; background-color: #eee; } @@ -115,6 +116,10 @@ var _staticCssSigninCss = []byte(`body { border-top-left-radius: 0; border-top-right-radius: 0; } +.error { + font-weight: bold; + color: red; +} `) func staticCssSigninCssBytes() ([]byte, error) { @@ -127,7 +132,7 @@ func staticCssSigninCss() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "static/css/signin.css", size: 756, mode: os.FileMode(420), modTime: time.Unix(1509120975, 0)} + info := bindataFileInfo{name: "static/css/signin.css", size: 802, mode: os.FileMode(436), modTime: time.Unix(1511081405, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -149,7 +154,7 @@ func staticJsBootstrap400BetaMinJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "static/js/bootstrap-4.0.0-beta.min.js", size: 51143, mode: os.FileMode(420), modTime: time.Unix(1509120962, 0)} + info := bindataFileInfo{name: "static/js/bootstrap-4.0.0-beta.min.js", size: 51143, mode: os.FileMode(436), modTime: time.Unix(1510996183, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -170,7 +175,7 @@ func staticJsJquery321MinJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "static/js/jquery-3.2.1.min.js", size: 86659, mode: os.FileMode(420), modTime: time.Unix(1509120962, 0)} + info := bindataFileInfo{name: "static/js/jquery-3.2.1.min.js", size: 86659, mode: os.FileMode(436), modTime: time.Unix(1510996183, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -192,7 +197,7 @@ func staticJsPopper1110MinJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "static/js/popper-1.11.0.min.js", size: 19033, mode: os.FileMode(420), modTime: time.Unix(1509120962, 0)} + info := bindataFileInfo{name: "static/js/popper-1.11.0.min.js", size: 19033, mode: os.FileMode(436), modTime: time.Unix(1510996183, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -957,7 +962,7 @@ func staticJsU2fApiJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "static/js/u2f-api.js", size: 20880, mode: os.FileMode(420), modTime: time.Unix(1509120962, 0)} + info := bindataFileInfo{name: "static/js/u2f-api.js", size: 20880, mode: os.FileMode(436), modTime: time.Unix(1510996183, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1026,7 +1031,7 @@ func staticJsU2fJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "static/js/u2f.js", size: 1281, mode: os.FileMode(420), modTime: time.Unix(1509260310, 0)} + info := bindataFileInfo{name: "static/js/u2f.js", size: 1281, mode: os.FileMode(436), modTime: time.Unix(1510996183, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1064,7 +1069,7 @@ func templatesLogin_otpHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "templates/login_otp.html", size: 529, mode: os.FileMode(420), modTime: time.Unix(1509218738, 0)} + info := bindataFileInfo{name: "templates/login_otp.html", size: 529, mode: os.FileMode(436), modTime: time.Unix(1510996183, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1124,7 +1129,7 @@ func templatesLogin_passwordHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "templates/login_password.html", size: 1074, mode: os.FileMode(420), modTime: time.Unix(1509218731, 0)} + info := bindataFileInfo{name: "templates/login_password.html", size: 1074, mode: os.FileMode(436), modTime: time.Unix(1510996183, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1164,7 +1169,65 @@ func templatesLogin_u2fHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "templates/login_u2f.html", size: 498, mode: os.FileMode(420), modTime: time.Unix(1509260387, 0)} + info := bindataFileInfo{name: "templates/login_u2f.html", size: 498, mode: os.FileMode(436), modTime: time.Unix(1510996183, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _templatesLogoutHtml = []byte(`{{template "header" .}} + +{{if .IsPOST}} + <div class="form-signin"> + <h1 class="form-signin-heading>">Sign Out</h1> + + <p> + Signing you out from all services... + </p> + + <ul> + {{range .Services}} + <li> + <img src="{{.URL}}" class="logout-img"> {{.Name}} + </li> + {{end}} + </ul> + + </div> +{{else}} + <form class="form-signin" action="/logout" method="post"> + {{.CSRFField}} + + <h1 class="form-signin-heading">Sign Out</h1> + + <p> + You are about to sign out from the following services: + </p> + + <ul> + {{range .Services}} + <li>{{.Name}}</li> + {{end}} + </ul> + + <button type="submit" class="btn btn-lg btn-primary btn-block">Logout</button> + + </form> +{{end}} + +{{template "footer" .}} +`) + +func templatesLogoutHtmlBytes() ([]byte, error) { + return _templatesLogoutHtml, nil +} + +func templatesLogoutHtml() (*asset, error) { + bytes, err := templatesLogoutHtmlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "templates/logout.html", size: 820, mode: os.FileMode(436), modTime: time.Unix(1511083629, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1209,7 +1272,7 @@ func templatesPageHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "templates/page.html", size: 1493, mode: os.FileMode(420), modTime: time.Unix(1509174553, 0)} + info := bindataFileInfo{name: "templates/page.html", size: 1493, mode: os.FileMode(436), modTime: time.Unix(1510996183, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1276,6 +1339,7 @@ var _bindata = map[string]func() (*asset, error){ "templates/login_otp.html": templatesLogin_otpHtml, "templates/login_password.html": templatesLogin_passwordHtml, "templates/login_u2f.html": templatesLogin_u2fHtml, + "templates/logout.html": templatesLogoutHtml, "templates/page.html": templatesPageHtml, } @@ -1336,6 +1400,7 @@ var _bintree = &bintree{nil, map[string]*bintree{ "login_otp.html": &bintree{templatesLogin_otpHtml, map[string]*bintree{}}, "login_password.html": &bintree{templatesLogin_passwordHtml, map[string]*bintree{}}, "login_u2f.html": &bintree{templatesLogin_u2fHtml, map[string]*bintree{}}, + "logout.html": &bintree{templatesLogoutHtml, map[string]*bintree{}}, "page.html": &bintree{templatesPageHtml, map[string]*bintree{}}, }}, }} diff --git a/server/http.go b/server/http.go index a00c72c6f64ea5a3ba812bec73b1e2685a559cfb..464cda6f7cef03d8cedbca9f8ef35fdaa1ec1fb8 100644 --- a/server/http.go +++ b/server/http.go @@ -5,6 +5,7 @@ package server import ( "encoding/gob" "fmt" + "html/template" "io" "log" "net/http" @@ -66,6 +67,19 @@ func init() { prometheus.MustRegister(totalRequests, inFlightRequests) } +// Returns the URL of the login handler on the target service. +func serviceLoginCallback(service, destination, token string) string { + v := make(url.Values) + v.Set("t", token) + v.Set("d", destination) + return fmt.Sprintf("https://%ssso_login?%s", service, v.Encode()) +} + +// Returns the URL of the logout handler on the target service. +func serviceLogoutCallback(service string) string { + return fmt.Sprintf("https://%ssso_logout", service) +} + // Server for the SSO protocol. Provides the HTTP interface to a // LoginService. type Server struct { @@ -74,6 +88,7 @@ type Server struct { loginHandler *loginHandler loginService *LoginService csrfSecret []byte + tpl *template.Template } func sl2bl(sl []string) [][]byte { @@ -98,6 +113,7 @@ func New(loginService *LoginService, authClient authclient.Client, config *Confi authSessionLifetime: defaultAuthSessionLifetime, authSessionStore: store, loginService: loginService, + tpl: parseEmbeddedTemplates(), } if config.CSRFSecret != "" { s.csrfSecret = []byte(config.CSRFSecret) @@ -110,7 +126,7 @@ func New(loginService *LoginService, authClient authclient.Client, config *Confi if err != nil { return nil, err } - s.loginHandler = newLoginHandler(s.loginCallback, devMgr, authClient, config.AuthService, sessionSecrets...) + s.loginHandler = newLoginHandler(s.loginCallback, devMgr, authClient, config.AuthService, s.tpl, sessionSecrets...) return s, nil } @@ -186,16 +202,39 @@ func (h *Server) handleHomepage(w http.ResponseWriter, req *http.Request, sessio _ = sessions.Save(req, w) // Redirect to service callback. - callbackURL := serviceCallback(service, destination, token) + callbackURL := serviceLoginCallback(service, destination, token) http.Redirect(w, req, callbackURL, http.StatusFound) } -// Returns the URL of the login handler on the target service. -func serviceCallback(service, destination, token string) string { - v := make(url.Values) - v.Set("t", token) - v.Set("d", destination) - return fmt.Sprintf("https://%ssso_login?%s", service, v.Encode()) +type logoutServiceInfo struct { + URL string + Name string +} + +func (h *Server) handleLogout(w http.ResponseWriter, req *http.Request, session *authSession) { + var svcs []logoutServiceInfo + for _, svc := range session.Services { + svcs = append(svcs, logoutServiceInfo{ + Name: svc, + URL: serviceLogoutCallback(svc), + }) + } + + data := map[string]interface{}{ + "CSRFField": csrf.TemplateField(req), + "Services": svcs, + "IsPOST": false, + } + if req.Method == "POST" { + data["IsPOST"] = true + + // Clear the local session. + httpSession, _ := h.authSessionStore.Get(req, authSessionKey) + httpSession.Options.MaxAge = -1 + _ = httpSession.Save(req, w) + } + + h.tpl.ExecuteTemplate(w, "logout.html", data) } func (h *Server) handleExchange(w http.ResponseWriter, req *http.Request) { @@ -221,13 +260,16 @@ func (h *Server) handleExchange(w http.ResponseWriter, req *http.Request) { func (h *Server) Handler() http.Handler { m := mux.NewRouter() - var lh http.Handler + var lih, loh http.Handler + lih = h.loginHandler + loh = h.withAuth(h.handleLogout) if h.csrfSecret != nil { - lh = csrf.Protect(h.csrfSecret)(h.loginHandler) - } else { - lh = h.loginHandler + csrfW := csrf.Protect(h.csrfSecret) + lih = csrfW(lih) + loh = csrfW(loh) } - m.Handle("/login", withDynamicHeaders(lh)) + m.Handle("/login", withDynamicHeaders(lih)) + m.Handle("/logout", withDynamicHeaders(loh)) m.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(&assetfs.AssetFS{ Asset: Asset, @@ -276,3 +318,24 @@ var ( }, ) ) + +// Parse the templates that are embedded with the binary (in bindata.go). +func parseEmbeddedTemplates() *template.Template { + root := template.New("").Funcs(template.FuncMap{ + "json": toJSON, + }) + files, err := AssetDir("templates") + if err != nil { + log.Fatalf("no asset dir for templates: %v", err) + } + for _, f := range files { + b, err := Asset("templates/" + f) + if err != nil { + log.Fatalf("could not read embedded template %s: %v", f, err) + } + if _, err := root.New(f).Parse(string(b)); err != nil { + log.Fatalf("error parsing template %s: %v", f, err) + } + } + return root +} diff --git a/server/login.go b/server/login.go index f79848f762220ee78c39a00a169a285d6ce58994..90005486789538740f72ef9aea7a423a9c5c1b3c 100644 --- a/server/login.go +++ b/server/login.go @@ -79,7 +79,7 @@ type loginHandler struct { // 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 string, keyPairs ...[]byte) *loginHandler { +func newLoginHandler(okHandler loginCallbackFunc, devMgr *device.Manager, authClient authclient.Client, authService string, tpl *template.Template, keyPairs ...[]byte) *loginHandler { store := sessions.NewCookieStore(keyPairs...) store.Options = &sessions.Options{ HttpOnly: true, @@ -288,27 +288,6 @@ func (l *loginHandler) executeTemplateToBuffer(req *http.Request, templateName s return loginStateNone, buf.Bytes(), nil } -// Parse the templates that are embedded with the binary (in bindata.go). -func parseEmbeddedTemplates() *template.Template { - root := template.New("").Funcs(template.FuncMap{ - "json": toJSON, - }) - files, err := AssetDir("templates") - if err != nil { - log.Fatalf("no asset dir for templates: %v", err) - } - for _, f := range files { - b, err := Asset("templates/" + f) - if err != nil { - log.Fatalf("could not read embedded template %s: %v", f, err) - } - if _, err := root.New(f).Parse(string(b)); err != nil { - log.Fatalf("error parsing template %s: %v", f, err) - } - } - return root -} - // Template helper function that encodes its input as JSON. func toJSON(obj interface{}) string { data, err := json.Marshal(obj) diff --git a/server/static/css/signin.css b/server/static/css/signin.css index 919a02a06c2bd39a52c121447b35b2fe347a1dbc..8fc53328e738560616a05438f25059d7a2845d30 100644 --- a/server/static/css/signin.css +++ b/server/static/css/signin.css @@ -1,5 +1,5 @@ body { - padding-top: 20%; + padding-top: 15%; padding-bottom: 20%; background-color: #eee; } @@ -36,3 +36,7 @@ body { border-top-left-radius: 0; border-top-right-radius: 0; } +.error { + font-weight: bold; + color: red; +} diff --git a/server/templates/logout.html b/server/templates/logout.html new file mode 100644 index 0000000000000000000000000000000000000000..7a0828020faf79d37465f84f0c61583ddfd02678 --- /dev/null +++ b/server/templates/logout.html @@ -0,0 +1,41 @@ +{{template "header" .}} + +{{if .IsPOST}} + <div class="form-signin"> + <h1 class="form-signin-heading>">Sign Out</h1> + + <p> + Signing you out from all services... + </p> + + <ul> + {{range .Services}} + <li> + <img src="{{.URL}}" class="logout-img"> {{.Name}} + </li> + {{end}} + </ul> + + </div> +{{else}} + <form class="form-signin" action="/logout" method="post"> + {{.CSRFField}} + + <h1 class="form-signin-heading">Sign Out</h1> + + <p> + You are about to sign out from the following services: + </p> + + <ul> + {{range .Services}} + <li>{{.Name}}</li> + {{end}} + </ul> + + <button type="submit" class="btn btn-lg btn-primary btn-block">Logout</button> + + </form> +{{end}} + +{{template "footer" .}}