diff --git a/server/bindata.go b/server/bindata.go
index 05947a42d60c3428f23deaffeffda4db352cd6a3..3b1a761c5f504fc2b7707e6907ef36b6781f1c29 100644
--- a/server/bindata.go
+++ b/server/bindata.go
@@ -1124,6 +1124,14 @@ var _templatesLogin_otpHtml = []byte(`{{template "header" .}}
 
     </form>
 
+    {{if .AuthResponse.Has2FAMethod "u2f"}}
+<p>
+  <a href="{{.URLPrefix}}/login?2fa=u2f">
+    Use a hardware token instead.
+  </a>
+</p>
+    {{end}}
+
 {{template "footer" .}}
 `)
 
@@ -1137,7 +1145,7 @@ func templatesLogin_otpHtml() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "templates/login_otp.html", size: 562, mode: os.FileMode(420), modTime: time.Unix(1550307595, 0)}
+	info := bindataFileInfo{name: "templates/login_otp.html", size: 711, mode: os.FileMode(420), modTime: time.Unix(1556965814, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -1226,6 +1234,14 @@ var _templatesLogin_u2fHtml = []byte(`{{template "header" .}}
       
     </form>
 
+    {{if .AuthResponse.Has2FAMethod "otp"}}
+<p>
+  <a href="{{.URLPrefix}}/login?2fa=otp">
+    Use a numeric one-time token instead.
+  </a>
+</p>
+    {{end}}
+
 {{template "footer" .}}
 `)
 
@@ -1239,7 +1255,7 @@ func templatesLogin_u2fHtml() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "templates/login_u2f.html", size: 512, mode: os.FileMode(420), modTime: time.Unix(1541234815, 0)}
+	info := bindataFileInfo{name: "templates/login_u2f.html", size: 669, mode: os.FileMode(420), modTime: time.Unix(1556965831, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
diff --git a/server/login.go b/server/login.go
index e511e8ee29a984e8f11551a237993a674a00863e..f82cca982310eb47eea279a564af92f38e26f52d 100644
--- a/server/login.go
+++ b/server/login.go
@@ -39,7 +39,9 @@ type loginSession struct {
 	AuthResponse *auth.Response
 }
 
-var defaultLoginSessionLifetime = 300 * time.Second
+// The login session is short-lived, it only needs to last for the duration of
+// the login process itself.
+var defaultLoginSessionLifetime = 10 * time.Minute
 
 func newLoginSession() *loginSession {
 	return &loginSession{
@@ -189,10 +191,8 @@ func (l *loginHandler) dispatch(w http.ResponseWriter, req *http.Request, sessio
 	switch session.State {
 	case loginStatePassword:
 		return l.handlePassword(w, req, session)
-	case loginStateOTP:
-		return l.handleOTP(w, req, session)
-	case loginStateU2F:
-		return l.handleU2F(w, req, session)
+	case loginStateOTP, loginStateU2F:
+		return l.handle2FA(w, req, session)
 	}
 	return loginStateNone, nil, errors.New("unreachable")
 }
@@ -241,11 +241,39 @@ func (l *loginHandler) handlePassword(w http.ResponseWriter, req *http.Request,
 	return loginStateNone, body, err
 }
 
+func (l *loginHandler) handle2FA(w http.ResponseWriter, req *http.Request, session *loginSession) (loginState, []byte, error) {
+	// The '2fa' request parameter can be used to manually switch between
+	// 2fa mechanisms. There is no need to pass the parameter through POSTs
+	// though, as the login session state is sticky.
+	if switch2fa := auth.TFAMethod(req.FormValue("2fa")); switch2fa != "" {
+		if !session.AuthResponse.Has2FAMethod(switch2fa) {
+			return loginStateNone, nil, errors.New("unsupported 2FA method")
+		}
+		switch switch2fa {
+		case auth.TFAMethodOTP:
+			session.State = loginStateOTP
+		case auth.TFAMethodU2F:
+			session.State = loginStateU2F
+		}
+	}
+
+	switch session.State {
+	case loginStateOTP:
+		return l.handleOTP(w, req, session)
+	case loginStateU2F:
+		return l.handleU2F(w, req, session)
+	}
+	return loginStateNone, nil, errors.New("unreachable")
+}
+
 // Handle login with password and TOTP.
 func (l *loginHandler) handleOTP(w http.ResponseWriter, req *http.Request, session *loginSession) (loginState, []byte, error) {
 	otp := req.FormValue("otp")
 
-	env := map[string]interface{}{"Error": false}
+	env := map[string]interface{}{
+		"AuthResponse": session.AuthResponse,
+		"Error":        false,
+	}
 	if req.Method == "POST" && otp != "" {
 		resp, err := l.makeAuthRequest(w, req, session.Username, session.Password, otp, nil)
 		if err != nil {
@@ -267,6 +295,7 @@ func (l *loginHandler) handleU2F(w http.ResponseWriter, req *http.Request, sessi
 	u2fresponse := req.FormValue("u2f_response")
 
 	env := map[string]interface{}{
+		"AuthResponse":   session.AuthResponse,
 		"U2FSignRequest": session.AuthResponse.U2FSignRequest,
 		"Error":          false,
 	}
diff --git a/server/templates/login_otp.html b/server/templates/login_otp.html
index 3a477b3aab2622c67e21aec74aa164b2b32bf270..6918a13f3af467384f579c8b48a9b25688a5ae8c 100644
--- a/server/templates/login_otp.html
+++ b/server/templates/login_otp.html
@@ -18,4 +18,12 @@
 
     </form>
 
+    {{if .AuthResponse.Has2FAMethod "u2f"}}
+<p>
+  <a href="{{.URLPrefix}}/login?2fa=u2f">
+    Use a hardware token instead.
+  </a>
+</p>
+    {{end}}
+
 {{template "footer" .}}
diff --git a/server/templates/login_u2f.html b/server/templates/login_u2f.html
index a023df44d29a7effa8f568d636d722bf944e7b4f..1386f1ace3854cd8b0b45aeb8a2be199ea17bf75 100644
--- a/server/templates/login_u2f.html
+++ b/server/templates/login_u2f.html
@@ -20,4 +20,12 @@
       
     </form>
 
+    {{if .AuthResponse.Has2FAMethod "otp"}}
+<p>
+  <a href="{{.URLPrefix}}/login?2fa=otp">
+    Use a numeric one-time token instead.
+  </a>
+</p>
+    {{end}}
+
 {{template "footer" .}}