Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Two factor authentication support #630

Merged
merged 11 commits into from
Jan 16, 2017
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions cmd/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,12 @@ func runWeb(ctx *cli.Context) error {
m.Post("/sign_up", bindIgnErr(auth.RegisterForm{}), user.SignUpPost)
m.Get("/reset_password", user.ResetPasswd)
m.Post("/reset_password", user.ResetPasswdPost)
m.Group("/2fa", func() {
m.Get("", user.ShowTwofa)
m.Post("", bindIgnErr(auth.TwofaAuthForm{}), user.TwofaPost)
m.Get("/scratch", user.TwofaScratch)
m.Post("/scratch", bindIgnErr(auth.TwofaScratchAuthForm{}), user.TwofaScratchPost)
})
}, reqSignOut)

m.Group("/user/settings", func() {
Expand All @@ -223,6 +229,13 @@ func runWeb(ctx *cli.Context) error {
Post(bindIgnErr(auth.NewAccessTokenForm{}), user.SettingsApplicationsPost)
m.Post("/applications/delete", user.SettingsDeleteApplication)
m.Route("/delete", "GET,POST", user.SettingsDelete)
m.Group("/2fa", func() {
m.Get("", user.SettingsTwofa)
m.Post("/regenerate_scratch", user.SettingsTwofaRegenerateScratch)
m.Post("/disable", user.SettingsTwofaDisable)
m.Get("/enroll", user.SettingsTwofaEnroll)
m.Post("/enroll", bindIgnErr(auth.TwofaAuthForm{}), user.SettingsTwofaEnrollPost)
})
}, reqSignIn, func(ctx *context.Context) {
ctx.Data["PageIsUserSettings"] = true
})
Expand Down
19 changes: 19 additions & 0 deletions models/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,25 @@ func (err ErrTeamAlreadyExist) Error() string {
return fmt.Sprintf("team already exists [org_id: %d, name: %s]", err.OrgID, err.Name)
}

//
// Two-factor authentication
//

// ErrTwofaNotEnrolled indicates that a user is not enrolled in two-factor authentication.
type ErrTwofaNotEnrolled struct {
UID int64
}

// IsErrTwofaNotEnrolled checks if an error is a ErrTwofaNotEnrolled.
func IsErrTwofaNotEnrolled(err error) bool {
_, ok := err.(ErrTwofaNotEnrolled)
return ok
}

func (err ErrTwofaNotEnrolled) Error() string {
return fmt.Sprintf("user not enrolled in 2FA [uid: %d]", err.UID)
}

// ____ ___ .__ .___
// | | \______ | | _________ __| _/
// | | /\____ \| | / _ \__ \ / __ |
Expand Down
1 change: 1 addition & 0 deletions models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ func init() {
new(Notification),
new(IssueUser),
new(LFSMetaObject),
new(Twofa),
)

gonicNames := []string{"SSL", "UID"}
Expand Down
141 changes: 141 additions & 0 deletions models/twofa.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// Copyright 2017 The Gitea 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 (
"crypto/md5"
"crypto/subtle"
"encoding/base64"
"time"

"github.com/Unknwon/com"
"github.com/go-xorm/xorm"
"github.com/pquerna/otp/totp"

"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/setting"
)

// Twofa represents a two-factor authentication token.
type Twofa struct {
ID int64 `xorm:"pk autoincr"`
UID int64 `xorm:"UNIQUE INDEX"`
Secret string
ScratchToken string

Created time.Time `xorm:"-"`
CreatedUnix int64 `xorm:"INDEX"`
Updated time.Time `xorm:"-"` // Note: Updated must below Created for AfterSet.
UpdatedUnix int64 `xorm:"INDEX"`
}

// BeforeInsert will be invoked by XORM before inserting a record representing this object.
func (t *Twofa) BeforeInsert() {
t.CreatedUnix = time.Now().Unix()
}

// BeforeUpdate is invoked from XORM before updating this object.
func (t *Twofa) BeforeUpdate() {
t.UpdatedUnix = time.Now().Unix()
}

// AfterSet is invoked from XORM after setting the value of a field of this object.
func (t *Twofa) AfterSet(colName string, _ xorm.Cell) {
switch colName {
case "created_unix":
t.Created = time.Unix(t.CreatedUnix, 0).Local()
case "updated_unix":
t.Updated = time.Unix(t.UpdatedUnix, 0).Local()
}
}

// GenerateScratchToken recreates the scratch token the user is using.
func (t *Twofa) GenerateScratchToken() error {
token, err := base.GetRandomString(8)
if err != nil {
return err
}
t.ScratchToken = token
return nil
}

// VerifyScratchToken verifies if the specified scratch token is valid.
func (t *Twofa) VerifyScratchToken(token string) bool {
if len(token) == 0 {
return false
}
return subtle.ConstantTimeCompare([]byte(token), []byte(t.ScratchToken)) == 1
}

func (t *Twofa) getEncryptionKey() []byte {
k := md5.Sum([]byte(setting.SecretKey))
return k[:]
}

// SetSecret sets the 2FA secret.
func (t *Twofa) SetSecret(secret string) error {
secretBytes, err := com.AESEncrypt(t.getEncryptionKey(), []byte(secret))
if err != nil {
return err
}
t.Secret = base64.StdEncoding.EncodeToString(secretBytes)
return nil
}

// ValidateTOTP validates the provided passcode.
func (t *Twofa) ValidateTOTP(passcode string) (bool, error) {
decodedStoredSecret, err := base64.StdEncoding.DecodeString(t.Secret)
if err != nil {
return false, err
}
secret, err := com.AESDecrypt(t.getEncryptionKey(), decodedStoredSecret)
if err != nil {
return false, err
}
secretStr := string(secret)
return totp.Validate(passcode, secretStr), nil
}

// NewTwofa creates a new two-factor authentication token.
func NewTwofa(t *Twofa) error {
err := t.GenerateScratchToken()
if err != nil {
return err
}
_, err = x.Insert(t)
return err
}

// UpdateTwofa updates a two-factor authentication token.
func UpdateTwofa(t *Twofa) error {
_, err := x.Id(t.ID).AllCols().Update(t)
return err
}

// GetTwofaByUID returns the two-factor authentication token associated with
// the user, if any.
func GetTwofaByUID(uid int64) (*Twofa, error) {
twofa := &Twofa{UID: uid}
has, err := x.Get(twofa)
if err != nil {
return nil, err
} else if !has {
return nil, ErrTwofaNotEnrolled{uid}
}
return twofa, nil
}

// DeleteTwofaByID deletes two-factor authentication token by given ID.
func DeleteTwofaByID(id, userID int64) error {
cnt, err := x.Id(id).Delete(&Twofa{
UID: userID,
})
if err != nil {
return err
} else if cnt != 1 {
return ErrTwofaNotEnrolled{userID}
}
return nil
}
20 changes: 20 additions & 0 deletions modules/auth/user_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,23 @@ type NewAccessTokenForm struct {
func (f *NewAccessTokenForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
return validate(errs, ctx.Data, f, ctx.Locale)
}

// TwofaAuthForm for logging in with 2FA token.
type TwofaAuthForm struct {
Passcode string `binding:"Required"`
}

// Validate valideates the fields
func (f *TwofaAuthForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
return validate(errs, ctx.Data, f, ctx.Locale)
}

// TwofaScratchAuthForm for logging in with 2FA scratch token.
type TwofaScratchAuthForm struct {
Token string `binding:"Required"`
}

// Validate valideates the fields
func (f *TwofaScratchAuthForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
return validate(errs, ctx.Data, f, ctx.Locale)
}
27 changes: 27 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ email = Email
password = Password
re_type = Re-Type
captcha = Captcha
twofa = Two-factor authentication
twofa_scratch = Two-factor scratch code
passcode = Passcode

repository = Repository
organization = Organization
Expand Down Expand Up @@ -175,6 +178,12 @@ invalid_code = Sorry, your confirmation code has expired or not valid.
reset_password_helper = Click here to reset your password
password_too_short = Password length cannot be less then %d.
non_local_account = Non-local accounts cannot change passwords through Gitea.
verify = Verify
scratch_code = Scratch code
use_scratch_code = Use a scratch code
twofa_scratch_used = You have used your scratch code. You have been redirected to the two-factor settings page so you may remove your device enrollment or generate a new scratch code.
twofa_passcode_incorrect = Your passcode is not correct. If you misplaced your device, use your scratch code.
twofa_scratch_token_incorrect = Your scratch code is not correct.

[mail]
activate_account = Please activate your account
Expand Down Expand Up @@ -266,6 +275,7 @@ social = Social Accounts
applications = Applications
orgs = Organizations
delete = Delete Account
twofa = Two-Factor Authentication
uid = Uid

public_profile = Public Profile
Expand Down Expand Up @@ -351,6 +361,23 @@ access_token_deletion = Personal Access Token Deletion
access_token_deletion_desc = Delete this personal access token will remove all related accesses of application. Do you want to continue?
delete_token_success = Personal access token has been removed successfully! Don't forget to update your application as well.

twofa_desc = Gitea supports two-factor authentication to provide additional security for your account.
twofa_is_enrolled = Your account is <strong>enrolled</strong> into two-factor authentication.
twofa_not_enrolled = Your account is not currently enrolled into two-factor authentication.
twofa_disable = Disable two-factor authentication
twofa_scratch_token_regenerate = Regenerate scratch token
twofa_scratch_token_regenerated = Your scratch token has been regenerated. It is now %s. Keep it in a safe place.
twofa_enroll = Enroll into two-factor authentication
twofa_disable_note = If needed, you can disable two-factor authentication.
twofa_disable_desc = Disabling two-factor authentication will make your account less secure. Are you sure you want to proceed?
regenerate_scratch_token_desc = If you misplaced your scratch token, or had to use it to log in, you can reset it.
twofa_disabled = Two-factor authentication has been disabled.
scan_this_image = Scan this image with your authentication application:
or_enter_secret = Or enter the secret: %s
then_enter_passcode = Then enter the passcode the application gives you:
passcode_invalid = That passcode is invalid. Try again.
twofa_enrolled = Your account has now been enrolled in two-factor authentication. Make sure to save your scratch token (%s), as it will only be shown once!

delete_account = Delete Your Account
delete_prompt = The operation will delete your account permanently, and <strong>CANNOT</strong> be undone!
confirm_delete_account = Confirm Deletion
Expand Down
Loading