Skip to content

Commit

Permalink
Add support for FIDO U2F (go-gitea#3971)
Browse files Browse the repository at this point in the history
* Add support for U2F

Signed-off-by: Jonas Franz <info@jonasfranz.software>

* Add vendor library
Add missing translations

Signed-off-by: Jonas Franz <info@jonasfranz.software>

* Minor improvements

Signed-off-by: Jonas Franz <info@jonasfranz.software>

* Add U2F support for Firefox, Chrome (Android) by introducing a custom JS library
Add U2F error handling

Signed-off-by: Jonas Franz <info@jonasfranz.software>

* Add U2F login page to OAuth

Signed-off-by: Jonas Franz <info@jonasfranz.software>

* Move U2F user settings to a separate file

Signed-off-by: Jonas Franz <info@jonasfranz.software>

* Add unit tests for u2f model
Renamed u2f table name

Signed-off-by: Jonas Franz <info@jonasfranz.software>

* Fix problems caused by refactoring

Signed-off-by: Jonas Franz <info@jonasfranz.software>

* Add U2F documentation

Signed-off-by: Jonas Franz <info@jonasfranz.software>

* Remove not needed console.log-s

Signed-off-by: Jonas Franz <info@jonasfranz.software>

* Add default values to app.ini.sample
Add FIDO U2F to comparison

Signed-off-by: Jonas Franz <info@jonasfranz.software>
  • Loading branch information
jonasfranz authored and lafriks committed May 19, 2018
1 parent f933bcd commit 951309f
Show file tree
Hide file tree
Showing 34 changed files with 1,599 additions and 9 deletions.
10 changes: 9 additions & 1 deletion custom/conf/app.ini.sample
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ RESET_PASSWD_CODE_LIVE_MINUTES = 180
REGISTER_EMAIL_CONFIRM = false
; Disallow registration, only allow admins to create accounts.
DISABLE_REGISTRATION = false
; Allow registration only using third part services, it works only when DISABLE_REGISTRATION is false
; Allow registration only using third part services, it works only when DISABLE_REGISTRATION is false
ALLOW_ONLY_EXTERNAL_REGISTRATION = false
; User must sign in to view anything.
REQUIRE_SIGNIN_VIEW = false
Expand Down Expand Up @@ -570,6 +570,14 @@ MAX_RESPONSE_ITEMS = 50
LANGS = en-US,zh-CN,zh-HK,zh-TW,de-DE,fr-FR,nl-NL,lv-LV,ru-RU,ja-JP,es-ES,pt-BR,pl-PL,bg-BG,it-IT,fi-FI,tr-TR,cs-CZ,sr-SP,sv-SE,ko-KR
NAMES = English,简体中文,繁體中文(香港),繁體中文(台灣),Deutsch,français,Nederlands,latviešu,русский,日本語,español,português do Brasil,polski,български,italiano,suomi,Türkçe,čeština,српски,svenska,한국어

[U2F]
; Two Factor authentication with security keys
; https://developers.yubico.com/U2F/App_ID.html
APP_ID = %(PROTOCOL)s://%(DOMAIN)s:%(HTTP_PORT)s
; Comma seperated list of truisted facets
TRUSTED_FACETS = %(PROTOCOL)s://%(DOMAIN)s:%(HTTP_PORT)s


; Used for datetimepicker
[i18n.datelang]
en-US = en
Expand Down
4 changes: 4 additions & 0 deletions docs/content/doc/advanced/config-cheat-sheet.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,10 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
- `MAX_GIT_DIFF_FILES`: **100**: Max number of files shown in diff view.
- `GC_ARGS`: **\<empty\>**: Arguments for command `git gc`, e.g. `--aggressive --auto`.

## U2F (`U2F`)
- `APP_ID`: **`ROOT_URL`**: Declares the facet of the application. Requires HTTPS.
- `TRUSTED_FACETS`: List of additional facets which are trusted. This is not support by all browsers.

## Markup (`markup`)

Gitea can support Markup using external tools. The example below will add a markup named `asciidoc`.
Expand Down
9 changes: 9 additions & 0 deletions docs/content/doc/features/comparison.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,15 @@ _Symbols used in table:_
<td>✓</td>
<td>✓</td>
</tr>
<tr>
<td>FIDO U2F (2FA)</td>
<td>✓</td>
<td>✘</td>
<td>✓</td>
<td>✓</td>
<td>✓</td>
<td>✓</td>
</tr>
<tr>
<td>Webhook support</td>
<td>✓</td>
Expand Down
22 changes: 22 additions & 0 deletions models/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -1237,3 +1237,25 @@ func IsErrExternalLoginUserNotExist(err error) bool {
func (err ErrExternalLoginUserNotExist) Error() string {
return fmt.Sprintf("external login user link does not exists [userID: %d, loginSourceID: %d]", err.UserID, err.LoginSourceID)
}

// ____ ________________________________ .__ __ __ .__
// | | \_____ \_ _____/\______ \ ____ ____ |__| _______/ |_____________ _/ |_|__| ____ ____
// | | // ____/| __) | _// __ \ / ___\| |/ ___/\ __\_ __ \__ \\ __\ |/ _ \ / \
// | | // \| \ | | \ ___// /_/ > |\___ \ | | | | \// __ \| | | ( <_> ) | \
// |______/ \_______ \___ / |____|_ /\___ >___ /|__/____ > |__| |__| (____ /__| |__|\____/|___| /
// \/ \/ \/ \/_____/ \/ \/ \/

// ErrU2FRegistrationNotExist represents a "ErrU2FRegistrationNotExist" kind of error.
type ErrU2FRegistrationNotExist struct {
ID int64
}

func (err ErrU2FRegistrationNotExist) Error() string {
return fmt.Sprintf("U2F registration does not exist [id: %d]", err.ID)
}

// IsErrU2FRegistrationNotExist checks if an error is a ErrU2FRegistrationNotExist.
func IsErrU2FRegistrationNotExist(err error) bool {
_, ok := err.(ErrU2FRegistrationNotExist)
return ok
}
7 changes: 7 additions & 0 deletions models/fixtures/u2f_registration.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-
id: 1
name: "U2F Key"
user_id: 1
counter: 0
created_unix: 946684800
updated_unix: 946684800
2 changes: 2 additions & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ var migrations = []Migration{
NewMigration("add language column for user setting", addLanguageSetting),
// v64 -> v65
NewMigration("add multiple assignees", addMultipleAssignees),
// v65 -> v66
NewMigration("add u2f", addU2FReg),
}

// Migrate database to current version
Expand Down
19 changes: 19 additions & 0 deletions models/migrations/v65.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package migrations

import (
"code.gitea.io/gitea/modules/util"
"github.com/go-xorm/xorm"
)

func addU2FReg(x *xorm.Engine) error {
type U2FRegistration struct {
ID int64 `xorm:"pk autoincr"`
Name string
UserID int64 `xorm:"INDEX"`
Raw []byte
Counter uint32
CreatedUnix util.TimeStamp `xorm:"INDEX created"`
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
}
return x.Sync2(&U2FRegistration{})
}
1 change: 1 addition & 0 deletions models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ func init() {
new(LFSLock),
new(Reaction),
new(IssueAssignees),
new(U2FRegistration),
)

gonicNames := []string{"SSL", "UID"}
Expand Down
120 changes: 120 additions & 0 deletions models/u2f.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright 2018 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 (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"

"github.com/tstranex/u2f"
)

// U2FRegistration represents the registration data and counter of a security key
type U2FRegistration struct {
ID int64 `xorm:"pk autoincr"`
Name string
UserID int64 `xorm:"INDEX"`
Raw []byte
Counter uint32
CreatedUnix util.TimeStamp `xorm:"INDEX created"`
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
}

// TableName returns a better table name for U2FRegistration
func (reg U2FRegistration) TableName() string {
return "u2f_registration"
}

// Parse will convert the db entry U2FRegistration to an u2f.Registration struct
func (reg *U2FRegistration) Parse() (*u2f.Registration, error) {
r := new(u2f.Registration)
return r, r.UnmarshalBinary(reg.Raw)
}

func (reg *U2FRegistration) updateCounter(e Engine) error {
_, err := e.ID(reg.ID).Cols("counter").Update(reg)
return err
}

// UpdateCounter will update the database value of counter
func (reg *U2FRegistration) UpdateCounter() error {
return reg.updateCounter(x)
}

// U2FRegistrationList is a list of *U2FRegistration
type U2FRegistrationList []*U2FRegistration

// ToRegistrations will convert all U2FRegistrations to u2f.Registrations
func (list U2FRegistrationList) ToRegistrations() []u2f.Registration {
regs := make([]u2f.Registration, len(list))
for _, reg := range list {
r, err := reg.Parse()
if err != nil {
log.Fatal(4, "parsing u2f registration: %v", err)
continue
}
regs = append(regs, *r)
}

return regs
}

func getU2FRegistrationsByUID(e Engine, uid int64) (U2FRegistrationList, error) {
regs := make(U2FRegistrationList, 0)
return regs, e.Where("user_id = ?", uid).Find(&regs)
}

// GetU2FRegistrationByID returns U2F registration by id
func GetU2FRegistrationByID(id int64) (*U2FRegistration, error) {
return getU2FRegistrationByID(x, id)
}

func getU2FRegistrationByID(e Engine, id int64) (*U2FRegistration, error) {
reg := new(U2FRegistration)
if found, err := e.ID(id).Get(reg); err != nil {
return nil, err
} else if !found {
return nil, ErrU2FRegistrationNotExist{ID: id}
}
return reg, nil
}

// GetU2FRegistrationsByUID returns all U2F registrations of the given user
func GetU2FRegistrationsByUID(uid int64) (U2FRegistrationList, error) {
return getU2FRegistrationsByUID(x, uid)
}

func createRegistration(e Engine, user *User, name string, reg *u2f.Registration) (*U2FRegistration, error) {
raw, err := reg.MarshalBinary()
if err != nil {
return nil, err
}
r := &U2FRegistration{
UserID: user.ID,
Name: name,
Counter: 0,
Raw: raw,
}
_, err = e.InsertOne(r)
if err != nil {
return nil, err
}
return r, nil
}

// CreateRegistration will create a new U2FRegistration from the given Registration
func CreateRegistration(user *User, name string, reg *u2f.Registration) (*U2FRegistration, error) {
return createRegistration(x, user, name, reg)
}

// DeleteRegistration will delete U2FRegistration
func DeleteRegistration(reg *U2FRegistration) error {
return deleteRegistration(x, reg)
}

func deleteRegistration(e Engine, reg *U2FRegistration) error {
_, err := e.Delete(reg)
return err
}
61 changes: 61 additions & 0 deletions models/u2f_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package models

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/tstranex/u2f"
)

func TestGetU2FRegistrationByID(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())

res, err := GetU2FRegistrationByID(1)
assert.NoError(t, err)
assert.Equal(t, "U2F Key", res.Name)

_, err = GetU2FRegistrationByID(342432)
assert.Error(t, err)
assert.True(t, IsErrU2FRegistrationNotExist(err))
}

func TestGetU2FRegistrationsByUID(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())

res, err := GetU2FRegistrationsByUID(1)
assert.NoError(t, err)
assert.Len(t, res, 1)
assert.Equal(t, "U2F Key", res[0].Name)
}

func TestU2FRegistration_TableName(t *testing.T) {
assert.Equal(t, "u2f_registration", U2FRegistration{}.TableName())
}

func TestU2FRegistration_UpdateCounter(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
reg := AssertExistsAndLoadBean(t, &U2FRegistration{ID: 1}).(*U2FRegistration)
reg.Counter = 1
assert.NoError(t, reg.UpdateCounter())
AssertExistsIf(t, true, &U2FRegistration{ID: 1, Counter: 1})
}

func TestCreateRegistration(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
user := AssertExistsAndLoadBean(t, &User{ID: 1}).(*User)

res, err := CreateRegistration(user, "U2F Created Key", &u2f.Registration{Raw: []byte("Test")})
assert.NoError(t, err)
assert.Equal(t, "U2F Created Key", res.Name)
assert.Equal(t, []byte("Test"), res.Raw)

AssertExistsIf(t, true, &U2FRegistration{Name: "U2F Created Key", UserID: user.ID})
}

func TestDeleteRegistration(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
reg := AssertExistsAndLoadBean(t, &U2FRegistration{ID: 1}).(*U2FRegistration)

assert.NoError(t, DeleteRegistration(reg))
AssertNotExistsBean(t, &U2FRegistration{ID: 1})
}
20 changes: 20 additions & 0 deletions modules/auth/user_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,23 @@ type TwoFactorScratchAuthForm struct {
func (f *TwoFactorScratchAuthForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
return validate(errs, ctx.Data, f, ctx.Locale)
}

// U2FRegistrationForm for reserving an U2F name
type U2FRegistrationForm struct {
Name string `binding:"Required"`
}

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

// U2FDeleteForm for deleting U2F keys
type U2FDeleteForm struct {
ID int64 `binding:"Required"`
}

// Validate valideates the fields
func (f *U2FDeleteForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
return validate(errs, ctx.Data, f, ctx.Locale)
}
8 changes: 8 additions & 0 deletions modules/setting/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,11 @@ var (
MaxResponseItems: 50,
}

U2F = struct {
AppID string
TrustedFacets []string
}{}

// I18n settings
Langs []string
Names []string
Expand Down Expand Up @@ -1135,6 +1140,9 @@ func NewContext() {
IsInputFile: sec.Key("IS_INPUT_FILE").MustBool(false),
})
}
sec = Cfg.Section("U2F")
U2F.TrustedFacets, _ = shellquote.Split(sec.Key("TRUSTED_FACETS").MustString(strings.TrimRight(AppURL, "/")))
U2F.AppID = sec.Key("APP_ID").MustString(strings.TrimRight(AppURL, "/"))
}

// Service settings
Expand Down
22 changes: 22 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,19 @@ twofa = Two-Factor Authentication
twofa_scratch = Two-Factor Scratch Code
passcode = Passcode

u2f_insert_key = Insert your security key
u2f_sign_in = Press the button on your security key. If you can't find a button, re-insert it.
u2f_press_button = Please press the button on your security key…
u2f_use_twofa = Use a two-factor code from your phone
u2f_error = We can't read your security key!
u2f_unsupported_browser = Your browser don't support U2F keys. Please try another browser.
u2f_error_1 = An unknown error occured. Please retry.
u2f_error_2 = Please make sure that you're using an encrypted connection (https://) and visiting the correct URL.
u2f_error_3 = The server could not proceed your request.
u2f_error_4 = The presented key is not eligible for this request. If you try to register it, make sure that the key isn't already registered.
u2f_error_5 = Timeout reached before your key could be read. Please reload to retry.
u2f_reload = Reload
repository = Repository
organization = Organization
mirror = Mirror
Expand Down Expand Up @@ -320,6 +333,7 @@ twofa = Two-Factor Authentication
account_link = Linked Accounts
organization = Organizations
uid = Uid
u2f = Security Keys

public_profile = Public Profile
profile_desc = Your email address will be used for notifications and other operations.
Expand Down Expand Up @@ -449,6 +463,14 @@ then_enter_passcode = And enter the passcode shown in the application:
passcode_invalid = The passcode is incorrect. Try again.
twofa_enrolled = Your account has been enrolled into two-factor authentication. Store your scratch token (%s) in a safe place as it is only shown once!

u2f_desc = Security keys are hardware devices containing cryptograhic keys. They could be used for two factor authentication. The security key must support the <a href="https://fidoalliance.org/">FIDO U2F</a> standard.
u2f_require_twofa = Two-Factor-Authentication must be enrolled in order to use security keys.
u2f_register_key = Add Security Key
u2f_nickname = Nickname
u2f_press_button = Press the button on your security key to register it.
u2f_delete_key = Remove Security Key
u2f_delete_key_desc= If you remove a security key you cannot login with it anymore. Are you sure?

manage_account_links = Manage Linked Accounts
manage_account_links_desc = These external accounts are linked to your Gitea account.
account_links_not_available = There are currently no external accounts linked to your Gitea account.
Expand Down
Loading

0 comments on commit 951309f

Please sign in to comment.