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" .}}