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