diff options
author | Unknwon <u@gogs.io> | 2017-04-06 00:14:30 -0400 |
---|---|---|
committer | Unknwon <u@gogs.io> | 2017-04-06 00:14:30 -0400 |
commit | a617d52374e937db0edacfba2a26bdd14a05538e (patch) | |
tree | 8c6448af2e4d68975ea66656b430e3b755e852c2 /routers | |
parent | 624474386aa51b74fdbee04bb85aaa957a22b99c (diff) |
2fa: initial support (#945)
Diffstat (limited to 'routers')
-rw-r--r-- | routers/repo/http.go | 9 | ||||
-rw-r--r-- | routers/user/auth.go | 186 | ||||
-rw-r--r-- | routers/user/setting.go | 165 |
3 files changed, 298 insertions, 62 deletions
diff --git a/routers/repo/http.go b/routers/repo/http.go index f90d1ce0..256ca16f 100644 --- a/routers/repo/http.go +++ b/routers/repo/http.go @@ -23,9 +23,9 @@ import ( "github.com/gogits/gogs/models" "github.com/gogits/gogs/models/errors" - "github.com/gogits/gogs/pkg/tool" "github.com/gogits/gogs/pkg/context" "github.com/gogits/gogs/pkg/setting" + "github.com/gogits/gogs/pkg/tool" ) const ( @@ -114,7 +114,6 @@ func HTTPContexter() macaron.Handler { authUser, err := models.UserSignIn(authUsername, authPassword) if err != nil && !errors.IsUserNotExist(err) { - c.Handle(http.StatusInternalServerError, "UserSignIn", err) return } @@ -139,6 +138,10 @@ func HTTPContexter() macaron.Handler { c.Handle(http.StatusInternalServerError, "GetUserByID", err) return } + } else if authUser.IsEnabledTwoFactor() { + askCredentials(c, http.StatusUnauthorized, `User with two-factor authentication enabled cannot perform HTTP/HTTPS operations via plain username and password +Please create and use personal access token on user settings page`) + return } log.Trace("HTTPGit - Authenticated user: %s", authUser.Name) @@ -152,7 +155,7 @@ func HTTPContexter() macaron.Handler { c.Handle(http.StatusInternalServerError, "HasAccess", err) return } else if !has { - askCredentials(c, http.StatusUnauthorized, "User permission denied") + askCredentials(c, http.StatusForbidden, "User permission denied") return } diff --git a/routers/user/auth.go b/routers/user/auth.go index bdbcb7e3..25c7988e 100644 --- a/routers/user/auth.go +++ b/routers/user/auth.go @@ -20,20 +20,22 @@ import ( ) const ( - SIGNIN = "user/auth/signin" - SIGNUP = "user/auth/signup" - ACTIVATE = "user/auth/activate" - FORGOT_PASSWORD = "user/auth/forgot_passwd" - RESET_PASSWORD = "user/auth/reset_passwd" + LOGIN = "user/auth/login" + TWO_FACTOR = "user/auth/two_factor" + TWO_FACTOR_RECOVERY_CODE = "user/auth/two_factor_recovery_code" + SIGNUP = "user/auth/signup" + ACTIVATE = "user/auth/activate" + FORGOT_PASSWORD = "user/auth/forgot_passwd" + RESET_PASSWORD = "user/auth/reset_passwd" ) -// AutoSignIn reads cookie and try to auto-login. -func AutoSignIn(ctx *context.Context) (bool, error) { +// AutoLogin reads cookie and try to auto-login. +func AutoLogin(c *context.Context) (bool, error) { if !models.HasEngine { return false, nil } - uname := ctx.GetCookie(setting.CookieUserName) + uname := c.GetCookie(setting.CookieUserName) if len(uname) == 0 { return false, nil } @@ -42,9 +44,9 @@ func AutoSignIn(ctx *context.Context) (bool, error) { defer func() { if !isSucceed { log.Trace("auto-login cookie cleared: %s", uname) - ctx.SetCookie(setting.CookieUserName, "", -1, setting.AppSubUrl) - ctx.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubUrl) - ctx.SetCookie(setting.LoginStatusCookieName, "", -1, setting.AppSubUrl) + c.SetCookie(setting.CookieUserName, "", -1, setting.AppSubUrl) + c.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubUrl) + c.SetCookie(setting.LoginStatusCookieName, "", -1, setting.AppSubUrl) } }() @@ -56,16 +58,16 @@ func AutoSignIn(ctx *context.Context) (bool, error) { return false, nil } - if val, ok := ctx.GetSuperSecureCookie(u.Rands+u.Passwd, setting.CookieRememberName); !ok || val != u.Name { + if val, ok := c.GetSuperSecureCookie(u.Rands+u.Passwd, setting.CookieRememberName); !ok || val != u.Name { return false, nil } isSucceed = true - ctx.Session.Set("uid", u.ID) - ctx.Session.Set("uname", u.Name) - ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubUrl) + c.Session.Set("uid", u.ID) + c.Session.Set("uname", u.Name) + c.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubUrl) if setting.EnableLoginStatusCookie { - ctx.SetCookie(setting.LoginStatusCookieName, "true", 0, setting.AppSubUrl) + c.SetCookie(setting.LoginStatusCookieName, "true", 0, setting.AppSubUrl) } return true, nil } @@ -77,77 +79,165 @@ func isValidRedirect(url string) bool { return len(url) >= 2 && url[0] == '/' && url[1] != '/' } -func SignIn(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("sign_in") +func Login(c *context.Context) { + c.Data["Title"] = c.Tr("sign_in") // Check auto-login. - isSucceed, err := AutoSignIn(ctx) + isSucceed, err := AutoLogin(c) if err != nil { - ctx.Handle(500, "AutoSignIn", err) + c.Handle(500, "AutoLogin", err) return } - redirectTo := ctx.Query("redirect_to") + redirectTo := c.Query("redirect_to") if len(redirectTo) > 0 { - ctx.SetCookie("redirect_to", redirectTo, 0, setting.AppSubUrl) + c.SetCookie("redirect_to", redirectTo, 0, setting.AppSubUrl) } else { - redirectTo, _ = url.QueryUnescape(ctx.GetCookie("redirect_to")) + redirectTo, _ = url.QueryUnescape(c.GetCookie("redirect_to")) } - ctx.SetCookie("redirect_to", "", -1, setting.AppSubUrl) + c.SetCookie("redirect_to", "", -1, setting.AppSubUrl) if isSucceed { if isValidRedirect(redirectTo) { - ctx.Redirect(redirectTo) + c.Redirect(redirectTo) } else { - ctx.Redirect(setting.AppSubUrl + "/") + c.Redirect(setting.AppSubUrl + "/") } return } - ctx.HTML(200, SIGNIN) + c.HTML(200, LOGIN) } -func SignInPost(ctx *context.Context, f form.SignIn) { - ctx.Data["Title"] = ctx.Tr("sign_in") +func afterLogin(c *context.Context, u *models.User, remember bool) { + if remember { + days := 86400 * setting.LoginRememberDays + c.SetCookie(setting.CookieUserName, u.Name, days, setting.AppSubUrl, "", setting.CookieSecure, true) + c.SetSuperSecureCookie(u.Rands+u.Passwd, setting.CookieRememberName, u.Name, days, setting.AppSubUrl, "", setting.CookieSecure, true) + } - if ctx.HasError() { - ctx.HTML(200, SIGNIN) + c.Session.Set("uid", u.ID) + c.Session.Set("uname", u.Name) + c.Session.Delete("twoFactorRemember") + c.Session.Delete("twoFactorUserID") + + // Clear whatever CSRF has right now, force to generate a new one + c.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubUrl) + if setting.EnableLoginStatusCookie { + c.SetCookie(setting.LoginStatusCookieName, "true", 0, setting.AppSubUrl) + } + + redirectTo, _ := url.QueryUnescape(c.GetCookie("redirect_to")) + c.SetCookie("redirect_to", "", -1, setting.AppSubUrl) + if isValidRedirect(redirectTo) { + c.Redirect(redirectTo) + return + } + + c.Redirect(setting.AppSubUrl + "/") +} + +func LoginPost(c *context.Context, f form.SignIn) { + c.Data["Title"] = c.Tr("sign_in") + + if c.HasError() { + c.Success(LOGIN) return } u, err := models.UserSignIn(f.UserName, f.Password) if err != nil { if errors.IsUserNotExist(err) { - ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), SIGNIN, &f) + c.RenderWithErr(c.Tr("form.username_password_incorrect"), LOGIN, &f) } else { - ctx.Handle(500, "UserSignIn", err) + c.ServerError("UserSignIn", err) } return } - if f.Remember { - days := 86400 * setting.LoginRememberDays - ctx.SetCookie(setting.CookieUserName, u.Name, days, setting.AppSubUrl, "", setting.CookieSecure, true) - ctx.SetSuperSecureCookie(u.Rands+u.Passwd, setting.CookieRememberName, u.Name, days, setting.AppSubUrl, "", setting.CookieSecure, true) + if !u.IsEnabledTwoFactor() { + afterLogin(c, u, f.Remember) + return } - ctx.Session.Set("uid", u.ID) - ctx.Session.Set("uname", u.Name) + c.Session.Set("twoFactorRemember", f.Remember) + c.Session.Set("twoFactorUserID", u.ID) + c.Redirect(setting.AppSubUrl + "/user/login/two_factor") +} - // Clear whatever CSRF has right now, force to generate a new one - ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubUrl) - if setting.EnableLoginStatusCookie { - ctx.SetCookie(setting.LoginStatusCookieName, "true", 0, setting.AppSubUrl) +func LoginTwoFactor(c *context.Context) { + _, ok := c.Session.Get("twoFactorUserID").(int64) + if !ok { + c.NotFound() + return } - redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")) - ctx.SetCookie("redirect_to", "", -1, setting.AppSubUrl) - if isValidRedirect(redirectTo) { - ctx.Redirect(redirectTo) + c.Success(TWO_FACTOR) +} + +func LoginTwoFactorPost(c *context.Context) { + userID, ok := c.Session.Get("twoFactorUserID").(int64) + if !ok { + c.NotFound() return } - ctx.Redirect(setting.AppSubUrl + "/") + t, err := models.GetTwoFactorByUserID(userID) + if err != nil { + c.ServerError("GetTwoFactorByUserID", err) + return + } + valid, err := t.ValidateTOTP(c.Query("passcode")) + if err != nil { + c.ServerError("ValidateTOTP", err) + return + } else if !valid { + c.Flash.Error(c.Tr("settings.two_factor_invalid_passcode")) + c.Redirect(setting.AppSubUrl + "/user/login/two_factor") + return + } + + u, err := models.GetUserByID(userID) + if err != nil { + c.ServerError("GetUserByID", err) + return + } + afterLogin(c, u, c.Session.Get("twoFactorRemember").(bool)) +} + +func LoginTwoFactorRecoveryCode(c *context.Context) { + _, ok := c.Session.Get("twoFactorUserID").(int64) + if !ok { + c.NotFound() + return + } + + c.Success(TWO_FACTOR_RECOVERY_CODE) +} + +func LoginTwoFactorRecoveryCodePost(c *context.Context) { + userID, ok := c.Session.Get("twoFactorUserID").(int64) + if !ok { + c.NotFound() + return + } + + if err := models.UseRecoveryCode(userID, c.Query("recovery_code")); err != nil { + if errors.IsTwoFactorRecoveryCodeNotFound(err) { + c.Flash.Error(c.Tr("auth.login_two_factor_invalid_recovery_code")) + c.Redirect(setting.AppSubUrl + "/user/login/two_factor_recovery_code") + } else { + c.ServerError("UseRecoveryCode", err) + } + return + } + + u, err := models.GetUserByID(userID) + if err != nil { + c.ServerError("GetUserByID", err) + return + } + afterLogin(c, u, c.Session.Get("twoFactorRemember").(bool)) } func SignOut(ctx *context.Context) { diff --git a/routers/user/setting.go b/routers/user/setting.go index 7eb5954f..9083454a 100644 --- a/routers/user/setting.go +++ b/routers/user/setting.go @@ -5,11 +5,17 @@ package user import ( + "bytes" + "encoding/base64" "fmt" + "html/template" + "image/png" "io/ioutil" "strings" "github.com/Unknwon/com" + "github.com/pquerna/otp" + "github.com/pquerna/otp/totp" log "gopkg.in/clog.v1" "github.com/gogits/gogs/models" @@ -22,17 +28,19 @@ import ( ) const ( - SETTINGS_PROFILE = "user/settings/profile" - SETTINGS_AVATAR = "user/settings/avatar" - SETTINGS_PASSWORD = "user/settings/password" - SETTINGS_EMAILS = "user/settings/email" - SETTINGS_SSH_KEYS = "user/settings/sshkeys" - SETTINGS_SECURITY = "user/settings/security" - SETTINGS_REPOSITORIES = "user/settings/repositories" - SETTINGS_ORGANIZATIONS = "user/settings/organizations" - SETTINGS_APPLICATIONS = "user/settings/applications" - SETTINGS_DELETE = "user/settings/delete" - NOTIFICATION = "user/notification" + SETTINGS_PROFILE = "user/settings/profile" + SETTINGS_AVATAR = "user/settings/avatar" + SETTINGS_PASSWORD = "user/settings/password" + SETTINGS_EMAILS = "user/settings/email" + SETTINGS_SSH_KEYS = "user/settings/sshkeys" + SETTINGS_SECURITY = "user/settings/security" + SETTINGS_TWO_FACTOR_ENABLE = "user/settings/two_factor_enable" + SETTINGS_TWO_FACTOR_RECOVERY_CODES = "user/settings/two_factor_recovery_codes" + SETTINGS_REPOSITORIES = "user/settings/repositories" + SETTINGS_ORGANIZATIONS = "user/settings/organizations" + SETTINGS_APPLICATIONS = "user/settings/applications" + SETTINGS_DELETE = "user/settings/delete" + NOTIFICATION = "user/notification" ) func Settings(c *context.Context) { @@ -376,6 +384,141 @@ func DeleteSSHKey(ctx *context.Context) { }) } +func SettingsSecurity(c *context.Context) { + c.Data["Title"] = c.Tr("settings") + c.Data["PageIsSettingsSecurity"] = true + + t, err := models.GetTwoFactorByUserID(c.UserID()) + if err != nil && !errors.IsTwoFactorNotFound(err) { + c.ServerError("GetTwoFactorByUserID", err) + return + } + c.Data["TwoFactor"] = t + + c.Success(SETTINGS_SECURITY) +} + +func SettingsTwoFactorEnable(c *context.Context) { + if c.User.IsEnabledTwoFactor() { + c.NotFound() + return + } + + c.Data["Title"] = c.Tr("settings") + c.Data["PageIsSettingsSecurity"] = true + + var key *otp.Key + var err error + keyURL := c.Session.Get("twoFactorURL") + if keyURL != nil { + key, _ = otp.NewKeyFromURL(keyURL.(string)) + } + if key == nil { + key, err = totp.Generate(totp.GenerateOpts{ + Issuer: setting.AppName, + AccountName: c.User.Email, + }) + if err != nil { + c.ServerError("Generate", err) + return + } + } + c.Data["TwoFactorSecret"] = key.Secret() + + img, err := key.Image(240, 240) + if err != nil { + c.ServerError("Image", err) + return + } + + var buf bytes.Buffer + if err = png.Encode(&buf, img); err != nil { + c.ServerError("Encode", err) + return + } + c.Data["QRCode"] = template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes())) + + c.Session.Set("twoFactorSecret", c.Data["TwoFactorSecret"]) + c.Session.Set("twoFactorURL", key.String()) + c.Success(SETTINGS_TWO_FACTOR_ENABLE) +} + +func SettingsTwoFactorEnablePost(c *context.Context) { + secret, ok := c.Session.Get("twoFactorSecret").(string) + if !ok { + c.NotFound() + return + } + + if !totp.Validate(c.Query("passcode"), secret) { + c.Flash.Error(c.Tr("settings.two_factor_invalid_passcode")) + c.Redirect(setting.AppSubUrl + "/user/settings/security/two_factor_enable") + return + } + + if err := models.NewTwoFactor(c.UserID(), secret); err != nil { + c.Flash.Error(c.Tr("settings.two_factor_enable_error", err)) + c.Redirect(setting.AppSubUrl + "/user/settings/security/two_factor_enable") + return + } + + c.Session.Delete("twoFactorSecret") + c.Session.Delete("twoFactorURL") + c.Flash.Success(c.Tr("settings.two_factor_enable_success")) + c.Redirect(setting.AppSubUrl + "/user/settings/security/two_factor_recovery_codes") +} + +func SettingsTwoFactorRecoveryCodes(c *context.Context) { + if !c.User.IsEnabledTwoFactor() { + c.NotFound() + return + } + + c.Data["Title"] = c.Tr("settings") + c.Data["PageIsSettingsSecurity"] = true + + recoveryCodes, err := models.GetRecoveryCodesByUserID(c.UserID()) + if err != nil { + c.ServerError("GetRecoveryCodesByUserID", err) + return + } + c.Data["RecoveryCodes"] = recoveryCodes + + c.Success(SETTINGS_TWO_FACTOR_RECOVERY_CODES) +} + +func SettingsTwoFactorRecoveryCodesPost(c *context.Context) { + if !c.User.IsEnabledTwoFactor() { + c.NotFound() + return + } + + if err := models.RegenerateRecoveryCodes(c.UserID()); err != nil { + c.Flash.Error(c.Tr("settings.two_factor_regenerate_recovery_codes_error", err)) + } else { + c.Flash.Success(c.Tr("settings.two_factor_regenerate_recovery_codes_success")) + } + + c.Redirect(setting.AppSubUrl + "/user/settings/security/two_factor_recovery_codes") +} + +func SettingsTwoFactorDisable(c *context.Context) { + if !c.User.IsEnabledTwoFactor() { + c.NotFound() + return + } + + if err := models.DeleteTwoFactor(c.UserID()); err != nil { + c.ServerError("DeleteTwoFactor", err) + return + } + + c.Flash.Success(c.Tr("settings.two_factor_disable_success")) + c.JSONSuccess(map[string]interface{}{ + "redirect": setting.AppSubUrl + "/user/settings/security", + }) +} + func SettingsApplications(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["PageIsSettingsApplications"] = true |