Browse Source

2fa: initial support (#945)

pull/4248/merge
Unknwon 8 years ago
parent
commit
a617d52374
No known key found for this signature in database
GPG Key ID: 25B575AE3213B2B3
  1. 3
      Makefile
  2. 17
      cmd/web.go
  3. 36
      conf/locale/locale_en-US.ini
  4. 2
      gogs.go
  5. 33
      models/errors/two_factor.go
  6. 4
      models/models.go
  7. 201
      models/two_factor.go
  8. 9
      models/user.go
  9. 4
      pkg/bindata/bindata.go
  10. 5
      pkg/context/context.go
  11. 11
      pkg/tool/tool.go
  12. 37
      public/css/gogs.css
  13. 15
      public/less/_form.less
  14. 25
      public/less/_user.less
  15. 9
      routers/repo/http.go
  16. 186
      routers/user/auth.go
  17. 165
      routers/user/setting.go
  18. 2
      templates/.VERSION
  19. 0
      templates/user/auth/login.tmpl
  20. 28
      templates/user/auth/two_factor.tmpl
  21. 28
      templates/user/auth/two_factor_recovery_code.tmpl
  22. 3
      templates/user/settings/navbar.tmpl
  23. 1
      templates/user/settings/profile.tmpl
  24. 51
      templates/user/settings/security.tmpl
  25. 28
      templates/user/settings/two_factor_enable.tmpl
  26. 36
      templates/user/settings/two_factor_recovery_codes.tmpl

3
Makefile

@ -25,6 +25,9 @@ check: test
dist: release
web: build
./gogs web
govet:
$(GOVET) gogs.go
$(GOVET) models pkg routers

17
cmd/web.go

@ -190,8 +190,13 @@ func runWeb(ctx *cli.Context) error {
// ***** START: User *****
m.Group("/user", func() {
m.Get("/login", user.SignIn)
m.Post("/login", bindIgnErr(form.SignIn{}), user.SignInPost)
m.Group("/login", func() {
m.Combo("").Get(user.Login).
Post(bindIgnErr(form.SignIn{}), user.LoginPost)
m.Combo("/two_factor").Get(user.LoginTwoFactor).Post(user.LoginTwoFactorPost)
m.Combo("/two_factor_recovery_code").Get(user.LoginTwoFactorRecoveryCode).Post(user.LoginTwoFactorRecoveryCodePost)
})
m.Get("/sign_up", user.SignUp)
m.Post("/sign_up", bindIgnErr(form.Register{}), user.SignUpPost)
m.Get("/reset_password", user.ResetPasswd)
@ -212,6 +217,14 @@ func runWeb(ctx *cli.Context) error {
m.Combo("/ssh").Get(user.SettingsSSHKeys).
Post(bindIgnErr(form.AddSSHKey{}), user.SettingsSSHKeysPost)
m.Post("/ssh/delete", user.DeleteSSHKey)
m.Group("/security", func() {
m.Get("", user.SettingsSecurity)
m.Combo("/two_factor_enable").Get(user.SettingsTwoFactorEnable).
Post(user.SettingsTwoFactorEnablePost)
m.Combo("/two_factor_recovery_codes").Get(user.SettingsTwoFactorRecoveryCodes).
Post(user.SettingsTwoFactorRecoveryCodesPost)
m.Post("/two_factor_disable", user.SettingsTwoFactorDisable)
})
m.Group("/repositories", func() {
m.Get("", user.SettingsRepos)
m.Post("/leave", user.SettingsLeaveRepo)

36
conf/locale/locale_en-US.ini

@ -168,6 +168,14 @@ reset_password_helper = Click here to reset your password
password_too_short = Password length cannot be less then 6.
non_local_account = Non-local accounts cannot change passwords through Gogs.
login_two_factor = Two-factor Authentication
login_two_factor_passcode = Authentication Passcode
login_two_factor_enter_recovery_code = Enter a two-factor recovery code
login_two_factor_recovery = Two-factor Recovery
login_two_factor_recovery_code = Recovery Code
login_two_factor_enter_passcode = Enter a two-factor passcode
login_two_factor_invalid_recovery_code = Recovery code has been used or does not valid.
[mail]
activate_account = Please activate your account
activate_email = Verify your email address
@ -255,6 +263,7 @@ profile = Profile
password = Password
avatar = Avatar
ssh_keys = SSH Keys
security = Security
repos = Repositories
orgs = Organizations
applications = Applications
@ -324,10 +333,29 @@ no_activity = No recent activity
key_state_desc = This key is used in last 7 days
token_state_desc = This token is used in last 7 days
manage_social = Manage Associated Social Accounts
social_desc = This is a list of associated social accounts. Remove any binding that you do not recognize.
unbind = Unbind
unbind_success = Social account has been unbound.
two_factor = Two-factor Authentication
two_factor_status = Status:
two_factor_on = On
two_factor_off = Off
two_factor_enable = Enable
two_factor_disable = Disable
two_factor_view_recovery_codes = View and save <a href="%s%s">your recovery codes</a> in a safe place. You can use them as passcode if you lose access to your authentication application.
two_factor_enable_title = Enable Two-factor Authentication
two_factor_scan_qr = Please use your authentication application to scan the image:
two_factor_or_enter_secret = Or enter the secret:
two_factor_then_enter_passcode = Then enter passcode:
two_factor_verify = Verify
two_factor_invalid_passcode = The passcode you entered is not valid, please try again!
two_factor_enable_error = Enable Two-factor authentication failed: %v
two_factor_enable_success = Two-factor authentication has enabled for your account successfully!
two_factor_recovery_codes_title = Two-factor Authentication Recovery Codes
two_factor_recovery_codes_desc = Recovery codes are used when you temporarily lose access to your authentication application. Each recovery code can only be used once, <b>please keep these codes in a safe place</b>.
two_factor_regenerate_recovery_codes = Regenerate Recovery Codes
two_factor_regenerate_recovery_codes_error = Regenerate recovery codes failed: %v
two_factor_regenerate_recovery_codes_success = New recovery codes has been generated successfully!
two_factor_disable_title = Disable Two-factor Authentication
two_factor_disable_desc = Your account security level will decrease after disabled two-factor authentication. Do you want to continue?
two_factor_disable_success = Two-factor authentication has disabled successfully!
manage_access_token = Manage Personal Access Tokens
generate_new_token = Generate New Token

2
gogs.go

@ -16,7 +16,7 @@ import (
"github.com/gogits/gogs/pkg/setting"
)
const APP_VER = "0.11.4.0405"
const APP_VER = "0.11.5.0406"
func init() {
setting.AppVer = APP_VER

33
models/errors/two_factor.go

@ -0,0 +1,33 @@
// Copyright 2017 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package errors
import "fmt"
type TwoFactorNotFound struct {
UserID int64
}
func IsTwoFactorNotFound(err error) bool {
_, ok := err.(TwoFactorNotFound)
return ok
}
func (err TwoFactorNotFound) Error() string {
return fmt.Sprintf("two-factor authentication does not found [user_id: %d]", err.UserID)
}
type TwoFactorRecoveryCodeNotFound struct {
Code string
}
func IsTwoFactorRecoveryCodeNotFound(err error) bool {
_, ok := err.(TwoFactorRecoveryCodeNotFound)
return ok
}
func (err TwoFactorRecoveryCodeNotFound) Error() string {
return fmt.Sprintf("two-factor recovery code does not found [code: %s]", err.Code)
}

4
models/models.go

@ -27,7 +27,7 @@ import (
"github.com/gogits/gogs/pkg/setting"
)
// Engine represents a xorm engine or session.
// Engine represents a XORM engine or session.
type Engine interface {
Delete(interface{}) (int64, error)
Exec(string, ...interface{}) (sql.Result, error)
@ -64,7 +64,7 @@ var (
func init() {
tables = append(tables,
new(User), new(PublicKey), new(AccessToken),
new(User), new(PublicKey), new(AccessToken), new(TwoFactor), new(TwoFactorRecoveryCode),
new(Repository), new(DeployKey), new(Collaboration), new(Access), new(Upload),
new(Watch), new(Star), new(Follow), new(Action),
new(Issue), new(PullRequest), new(Comment), new(Attachment), new(IssueUser),

201
models/two_factor.go

@ -0,0 +1,201 @@
// Copyright 2017 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package models
import (
"encoding/base64"
"fmt"
"strings"
"time"
"github.com/Unknwon/com"
"github.com/go-xorm/xorm"
"github.com/pquerna/otp/totp"
log "gopkg.in/clog.v1"
"github.com/gogits/gogs/models/errors"
"github.com/gogits/gogs/pkg/setting"
"github.com/gogits/gogs/pkg/tool"
)
// TwoFactor represents a two-factor authentication token.
type TwoFactor struct {
ID int64
UserID int64 `xorm:"UNIQUE"`
Secret string
Created time.Time `xorm:"-"`
CreatedUnix int64
}
func (t *TwoFactor) BeforeInsert() {
t.CreatedUnix = time.Now().Unix()
}
func (t *TwoFactor) AfterSet(colName string, _ xorm.Cell) {
switch colName {
case "created_unix":
t.Created = time.Unix(t.CreatedUnix, 0).Local()
}
}
// ValidateTOTP returns true if given passcode is valid for two-factor authentication token.
// It also returns possible validation error.
func (t *TwoFactor) ValidateTOTP(passcode string) (bool, error) {
secret, err := base64.StdEncoding.DecodeString(t.Secret)
if err != nil {
return false, fmt.Errorf("DecodeString: %v", err)
}
decryptSecret, err := com.AESGCMDecrypt(tool.MD5Bytes(setting.SecretKey), secret)
if err != nil {
return false, fmt.Errorf("AESGCMDecrypt: %v", err)
}
return totp.Validate(passcode, string(decryptSecret)), nil
}
// IsUserEnabledTwoFactor returns true if user has enabled two-factor authentication.
func IsUserEnabledTwoFactor(userID int64) bool {
has, err := x.Where("user_id = ?", userID).Get(new(TwoFactor))
if err != nil {
log.Error(2, "IsUserEnabledTwoFactor [user_id: %d]: %v", userID, err)
}
return has
}
func generateRecoveryCodes(userID int64) ([]*TwoFactorRecoveryCode, error) {
recoveryCodes := make([]*TwoFactorRecoveryCode, 10)
for i := 0; i < 10; i++ {
code, err := tool.GetRandomString(10)
if err != nil {
return nil, fmt.Errorf("GetRandomString: %v", err)
}
recoveryCodes[i] = &TwoFactorRecoveryCode{
UserID: userID,
Code: strings.ToLower(code[:5] + "-" + code[5:]),
}
}
return recoveryCodes, nil
}
// NewTwoFactor creates a new two-factor authentication token and recovery codes for given user.
func NewTwoFactor(userID int64, secret string) error {
t := &TwoFactor{
UserID: userID,
}
// Encrypt secret
encryptSecret, err := com.AESGCMEncrypt(tool.MD5Bytes(setting.SecretKey), []byte(secret))
if err != nil {
return fmt.Errorf("AESGCMEncrypt: %v", err)
}
t.Secret = base64.StdEncoding.EncodeToString(encryptSecret)
recoveryCodes, err := generateRecoveryCodes(userID)
if err != nil {
return fmt.Errorf("generateRecoveryCodes: %v", err)
}
sess := x.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
return err
}
if _, err = sess.Insert(t); err != nil {
return fmt.Errorf("insert two-factor: %v", err)
} else if _, err = sess.Insert(recoveryCodes); err != nil {
return fmt.Errorf("insert recovery codes: %v", err)
}
return sess.Commit()
}
// GetTwoFactorByUserID returns two-factor authentication token of given user.
func GetTwoFactorByUserID(userID int64) (*TwoFactor, error) {
t := new(TwoFactor)
has, err := x.Where("user_id = ?", userID).Get(t)
if err != nil {
return nil, err
} else if !has {
return nil, errors.TwoFactorNotFound{userID}
}
return t, nil
}
// DeleteTwoFactor removes two-factor authentication token and recovery codes of given user.
func DeleteTwoFactor(userID int64) (err error) {
sess := x.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
return err
}
if _, err = sess.Where("user_id = ?", userID).Delete(new(TwoFactor)); err != nil {
return fmt.Errorf("delete two-factor: %v", err)
} else if err = deleteRecoveryCodesByUserID(sess, userID); err != nil {
return fmt.Errorf("deleteRecoveryCodesByUserID: %v", err)
}
return sess.Commit()
}
// TwoFactorRecoveryCode represents a two-factor authentication recovery code.
type TwoFactorRecoveryCode struct {
ID int64
UserID int64
Code string `xorm:"VARCHAR(11)"`
IsUsed bool
}
// GetRecoveryCodesByUserID returns all recovery codes of given user.
func GetRecoveryCodesByUserID(userID int64) ([]*TwoFactorRecoveryCode, error) {
recoveryCodes := make([]*TwoFactorRecoveryCode, 0, 10)
return recoveryCodes, x.Where("user_id = ?", userID).Find(&recoveryCodes)
}
func deleteRecoveryCodesByUserID(e Engine, userID int64) error {
_, err := e.Where("user_id = ?", userID).Delete(new(TwoFactorRecoveryCode))
return err
}
// RegenerateRecoveryCodes regenerates new set of recovery codes for given user.
func RegenerateRecoveryCodes(userID int64) error {
recoveryCodes, err := generateRecoveryCodes(userID)
if err != nil {
return fmt.Errorf("generateRecoveryCodes: %v", err)
}
sess := x.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
return err
}
if err = deleteRecoveryCodesByUserID(sess, userID); err != nil {
return fmt.Errorf("deleteRecoveryCodesByUserID: %v", err)
} else if _, err = sess.Insert(recoveryCodes); err != nil {
return fmt.Errorf("insert new recovery codes: %v", err)
}
return sess.Commit()
}
// UseRecoveryCode validates recovery code of given user and marks it is used if valid.
func UseRecoveryCode(userID int64, code string) error {
recoveryCode := new(TwoFactorRecoveryCode)
has, err := x.Where("code = ?", code).And("is_used = ?", false).Get(recoveryCode)
if err != nil {
return fmt.Errorf("get unused code: %v", err)
} else if !has {
return errors.TwoFactorRecoveryCodeNotFound{code}
}
recoveryCode.IsUsed = true
if _, err = x.Id(recoveryCode.ID).Cols("is_used").Update(recoveryCode); err != nil {
return fmt.Errorf("mark code as used: %v", err)
}
return nil
}

9
models/user.go

@ -31,8 +31,8 @@ import (
"github.com/gogits/gogs/models/errors"
"github.com/gogits/gogs/pkg/avatar"
"github.com/gogits/gogs/pkg/tool"
"github.com/gogits/gogs/pkg/setting"
"github.com/gogits/gogs/pkg/tool"
)
type UserType int
@ -404,6 +404,11 @@ func (u *User) IsPublicMember(orgId int64) bool {
return IsPublicMembership(orgId, u.ID)
}
// IsEnabledTwoFactor returns true if user has enabled two-factor authentication.
func (u *User) IsEnabledTwoFactor() bool {
return IsUserEnabledTwoFactor(u.ID)
}
func (u *User) getOrganizationCount(e Engine) (int64, error) {
return e.Where("uid=?", u.ID).Count(new(OrgUser))
}
@ -479,7 +484,7 @@ func IsUserExist(uid int64, name string) (bool, error) {
if len(name) == 0 {
return false, nil
}
return x.Where("id!=?", uid).Get(&User{LowerName: strings.ToLower(name)})
return x.Where("id != ?", uid).Get(&User{LowerName: strings.ToLower(name)})
}
// GetUserSalt returns a ramdom user salt token.

4
pkg/bindata/bindata.go

File diff suppressed because one or more lines are too long

5
pkg/context/context.go

@ -89,6 +89,11 @@ func (c *Context) Success(name string) {
c.HTML(http.StatusOK, name)
}
// JSONSuccess responses JSON with status http.StatusOK.
func (c *Context) JSONSuccess(data interface{}) {
c.JSON(http.StatusOK, data)
}
// RenderWithErr used for page has form validation but need to prompt error to users.
func (ctx *Context) RenderWithErr(msg, tpl string, f interface{}) {
if f != nil {

11
pkg/tool/tool.go

@ -27,11 +27,16 @@ import (
"github.com/gogits/gogs/pkg/setting"
)
// EncodeMD5 encodes string to md5 hex value.
func EncodeMD5(str string) string {
// MD5Bytes encodes string to MD5 bytes.
func MD5Bytes(str string) []byte {
m := md5.New()
m.Write([]byte(str))
return hex.EncodeToString(m.Sum(nil))
return m.Sum(nil)
}
// EncodeMD5 encodes string to MD5 hex value.
func EncodeMD5(str string) string {
return hex.EncodeToString(MD5Bytes(str))
}
// Encode string to sha1 hex value.

37
public/css/gogs.css

@ -960,7 +960,7 @@ footer .ui.language .menu {
}
#create-page-form form input,
#create-page-form form textarea {
width: 50%!important;
width: 50% !important;
}
.user.activate form,
.user.forgot.password form,
@ -1017,14 +1017,14 @@ footer .ui.language .menu {
.user.reset.password form textarea,
.user.signin form textarea,
.user.signup form textarea {
width: 50%!important;
width: 50% !important;
}
.user.activate form,
.user.forgot.password form,
.user.reset.password form,
.user.signin form,
.user.signup form {
width: 700px!important;
width: 700px !important;
}
.user.activate form .header,
.user.forgot.password form .header,
@ -1040,6 +1040,12 @@ footer .ui.language .menu {
.user.signup form .inline.field > label {
width: 200px !important;
}
.user.signin.two-factor form {
width: 300px !important;
}
.user.signin.two-factor form .header {
padding-left: inherit !important;
}
.repository.new.repo form,
.repository.new.migrate form,
.repository.new.fork form {
@ -1079,7 +1085,7 @@ footer .ui.language .menu {
.repository.new.repo form textarea,
.repository.new.migrate form textarea,
.repository.new.fork form textarea {
width: 50%!important;
width: 50% !important;
}
.repository.new.repo form .dropdown .dropdown.icon,
.repository.new.migrate form .dropdown .dropdown.icon,
@ -2752,7 +2758,7 @@ footer .ui.language .menu {
}
.organization.new.org form input,
.organization.new.org form textarea {
width: 50%!important;
width: 50% !important;
}
.organization.options input {
min-width: 300px;
@ -2856,15 +2862,8 @@ footer .ui.language .menu {
.user.settings .email.list .item:not(:first-child) .button {
margin-top: -10px;
}
.user.settings.organizations .orgs.non-empty {
padding: 0;
}
.user.settings.organizations .orgs .item {
padding: 10px;
}
.user.settings.organizations .orgs .item .button {
margin-top: 5px;
margin-right: 8px;
.user.settings.security .two-factor .toggle.button {
margin-top: -5px;
}
.user.settings.repositories .repos {
padding: 0;
@ -2876,6 +2875,16 @@ footer .ui.language .menu {
.user.settings.repositories .repos .item .button {
margin-top: -5px;
}
.user.settings.organizations .orgs.non-empty {
padding: 0;
}
.user.settings.organizations .orgs .item {
padding: 10px;
}
.user.settings.organizations .orgs .item .button {
margin-top: 5px;
margin-right: 8px;
}
.user.profile .ui.card .header {
word-break: break-all;
}

15
public/less/_form.less

@ -42,7 +42,7 @@
}
input,
textarea {
width: 50%!important;
width: 50% !important;
}
}
}
@ -52,10 +52,10 @@
.user.reset.password,
.user.signin,
.user.signup {
@input-padding: 200px!important;
@input-padding: 200px !important;
#create-page-form;
form {
width: 700px!important;
width: 700px !important;
.header {
padding-left: @input-padding+30px;
}
@ -65,6 +65,15 @@
}
}
.user.signin.two-factor {
form {
width: 300px !important;
.header {
padding-left: inherit !important;
}
}
}
.repository {
&.new.repo,
&.new.migrate,

25
public/less/_user.less

@ -19,16 +19,9 @@
}
}
}
&.organizations .orgs {
&.non-empty {
padding: 0;
}
.item {
padding: 10px;
.button {
margin-top: 5px;
margin-right: 8px;
}
&.security {
.two-factor .toggle.button {
margin-top: -5px;
}
}
&.repositories .repos {
@ -41,6 +34,18 @@
}
}
}
&.organizations .orgs {
&.non-empty {
padding: 0;
}
.item {
padding: 10px;
.button {
margin-top: 5px;
margin-right: 8px;
}
}
}
}
&.profile {

9
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
}

186
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) {

165
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

2
templates/.VERSION

@ -1 +1 @@
0.11.4.0405
0.11.5.0406

0
templates/user/auth/signin.tmpl → templates/user/auth/login.tmpl

28
templates/user/auth/two_factor.tmpl

@ -0,0 +1,28 @@
{{template "base/head" .}}
<div class="user signin two-factor">
<div class="ui middle very relaxed page grid">
<div class="column">
<form class="ui form" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}}
<h3 class="ui top attached center header">
{{.i18n.Tr "auth.login_two_factor"}}
</h3>
<div class="ui attached segment">
{{template "base/alert" .}}
<div class="required field">
<label for="passcode">{{.i18n.Tr "auth.login_two_factor_passcode"}}</label>
<div class="ui fluid input">
<input id="passcode" name="passcode" autofocus required>
</div>
</div>
<button class="ui fluid green button">{{.i18n.Tr "settings.two_factor_verify"}}</button>
</div>
<p>
<a href="{{AppSubUrl}}/user/login/two_factor_recovery_code">{{.i18n.Tr "auth.login_two_factor_enter_recovery_code"}}</a>
</p>
</form>
</div>
</div>
</div>
{{template "base/footer" .}}

28
templates/user/auth/two_factor_recovery_code.tmpl

@ -0,0 +1,28 @@
{{template "base/head" .}}
<div class="user signin two-factor">
<div class="ui middle very relaxed page grid">
<div class="column">
<form class="ui form" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}}
<h3 class="ui top attached center header">
{{.i18n.Tr "auth.login_two_factor_recovery"}}
</h3>
<div class="ui attached segment">
{{template "base/alert" .}}
<div class="required field">
<label for="recovery_code">{{.i18n.Tr "auth.login_two_factor_recovery_code"}}</label>
<div class="ui fluid input">
<input id="recovery_code" name="recovery_code" autofocus required>
</div>
</div>
<button class="ui fluid green button">{{.i18n.Tr "settings.two_factor_verify"}}</button>
</div>
<p>
<a href="{{AppSubUrl}}/user/login/two_factor">{{.i18n.Tr "auth.login_two_factor_enter_passcode"}}</a>
</p>
</form>
</div>
</div>
</div>
{{template "base/footer" .}}

3
templates/user/settings/navbar.tmpl

@ -16,6 +16,9 @@
<a class="{{if .PageIsSettingsSSHKeys}}active{{end}} item" href="{{AppSubUrl}}/user/settings/ssh">
{{.i18n.Tr "settings.ssh_keys"}}
</a>
<a class="{{if .PageIsSettingsSecurity}}active{{end}} item" href="{{AppSubUrl}}/user/settings/security">
{{.i18n.Tr "settings.security"}}
</a>
<a class="{{if .PageIsSettingsRepositories}}active{{end}} item" href="{{AppSubUrl}}/user/settings/repositories">
{{.i18n.Tr "settings.repos"}}
</a>

1
templates/user/settings/profile.tmpl

@ -40,7 +40,6 @@
<button class="ui green button">{{$.i18n.Tr "settings.update_profile"}}</button>
</div>
</form>
</div>
</div>
</div>

51
templates/user/settings/security.tmpl

@ -0,0 +1,51 @@
{{template "base/head" .}}
<div class="user settings security">
<div class="ui container">
<div class="ui grid">
{{template "user/settings/navbar" .}}
<div class="twelve wide column content">
{{template "base/alert" .}}
<h4 class="ui top attached header">
{{.i18n.Tr "settings.two_factor"}}
</h4>
<div class="ui attached segment two-factor">
<p class="text bold">
{{.i18n.Tr "settings.two_factor_status"}}
{{if .TwoFactor}}
<span class="text green">{{.i18n.Tr "settings.two_factor_on"}} <i class="octicon octicon-check"></i></span>
<button class="ui right mini red toggle button delete-button" data-url="{{$.Link}}/two_factor_disable">{{.i18n.Tr "settings.two_factor_disable"}}</button>
{{else}}
<span class="text red">{{.i18n.Tr "settings.two_factor_off"}} <i class="octicon octicon-x"></i></span>
<a class="ui right mini green toggle button" href="{{AppSubUrl}}/user/settings/security/two_factor_enable">{{.i18n.Tr "settings.two_factor_enable"}}</a>
{{end}}
</p>
</div>
{{if .TwoFactor}}
<br>
<p>{{.i18n.Tr "settings.two_factor_view_recovery_codes" AppSubUrl "/user/settings/security/two_factor_recovery_codes" | Safe}}</p>
{{end}}
</div>
</div>
</div>
</div>
<div class="ui small basic delete modal">
<div class="ui icon header">
<i class="trash icon"></i>
{{.i18n.Tr "settings.two_factor_disable_title"}}
</div>
<div class="content">
<p>{{.i18n.Tr "settings.two_factor_disable_desc"}}</p>
</div>
<div class="actions">
<div class="ui red basic inverted cancel button">
<i class="remove icon"></i>
{{.i18n.Tr "modal.no"}}
</div>
<div class="ui green basic inverted ok button">
<i class="checkmark icon"></i>
{{.i18n.Tr "modal.yes"}}
</div>
</div>
</div>
{{template "base/footer" .}}

28
templates/user/settings/two_factor_enable.tmpl

@ -0,0 +1,28 @@
{{template "base/head" .}}
<div class="user settings security two-factor">
<div class="ui container">
<div class="ui grid">
{{template "user/settings/navbar" .}}
<div class="twelve wide column content">
{{template "base/alert" .}}
<h4 class="ui top attached header">
{{.i18n.Tr "settings.two_factor_enable_title"}}
</h4>
<div class="ui attached segment">
<div>{{.i18n.Tr "settings.two_factor_scan_qr"}}</div>
<img src="{{.QRCode}}" alt="{{.TwoFactorSecret}}">
<p>{{.i18n.Tr "settings.two_factor_or_enter_secret"}} <b>{{.TwoFactorSecret}}</b></p>
<form class="ui form" method="post">
{{.CsrfTokenHtml}}
<div class="required inline field">
<span>{{.i18n.Tr "settings.two_factor_then_enter_passcode"}}</span>
<input class="ui input" name="passcode" autocomplete="off" autofocus required>
</div>
<button class="ui green button">{{.i18n.Tr "settings.two_factor_verify"}}</button>
</form>
</div>
</div>
</div>
</div>
</div>
{{template "base/footer" .}}

36
templates/user/settings/two_factor_recovery_codes.tmpl

@ -0,0 +1,36 @@
{{template "base/head" .}}
<div class="user settings security two-factor">
<div class="ui container">
<div class="ui grid">
{{template "user/settings/navbar" .}}
<div class="twelve wide column content">
{{template "base/alert" .}}
<h4 class="ui top attached header">
{{.i18n.Tr "settings.two_factor_recovery_codes_title"}}
</h4>
<div class="ui attached segment">
<p>{{.i18n.Tr "settings.two_factor_recovery_codes_desc" | Safe}}</p>
<ul class="ui list">
{{range .RecoveryCodes}}
<li class="item">
<code>
{{if .IsUsed}}
<del>{{.Code}}</del>
{{else}}
{{.Code}}
{{end}}
</code>
</li>
{{end}}
</ul>
<form class="ui form" method="post">
{{.CsrfTokenHtml}}
<button class="ui blue button">{{.i18n.Tr "settings.two_factor_regenerate_recovery_codes"}}</button>
</form>
</div>
</div>
</div>
</div>
</div>
{{template "base/footer" .}}
Loading…
Cancel
Save