mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-01 05:36:19 +01:00
Allow U2F 2FA without TOTP (#11573)
This change enables the usage of U2F without being forced to enroll an TOTP authenticator. The `/user/auth/u2f` has been changed to hide the "use TOTP instead" bar if TOTP is not enrolled. Fixes #5410 Fixes #17495
This commit is contained in:
parent
a3f9e9234c
commit
021df29623
12 changed files with 100 additions and 57 deletions
|
@ -1,7 +1,7 @@
|
||||||
-
|
-
|
||||||
id: 1
|
id: 1
|
||||||
name: "U2F Key"
|
name: "U2F Key"
|
||||||
user_id: 1
|
user_id: 32
|
||||||
counter: 0
|
counter: 0
|
||||||
created_unix: 946684800
|
created_unix: 946684800
|
||||||
updated_unix: 946684800
|
updated_unix: 946684800
|
||||||
|
|
|
@ -542,3 +542,19 @@
|
||||||
avatar_email: user31@example.com
|
avatar_email: user31@example.com
|
||||||
num_repos: 0
|
num_repos: 0
|
||||||
is_active: true
|
is_active: true
|
||||||
|
|
||||||
|
-
|
||||||
|
id: 32
|
||||||
|
lower_name: user32
|
||||||
|
name: user32
|
||||||
|
full_name: User 32 (U2F test)
|
||||||
|
email: user32@example.com
|
||||||
|
passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
|
||||||
|
type: 0 # individual
|
||||||
|
salt: ZogKvWdyEx
|
||||||
|
is_admin: false
|
||||||
|
is_restricted: false
|
||||||
|
avatar: avatar32
|
||||||
|
avatar_email: user30@example.com
|
||||||
|
num_repos: 0
|
||||||
|
is_active: true
|
||||||
|
|
|
@ -136,6 +136,12 @@ func GetTwoFactorByUID(uid int64) (*TwoFactor, error) {
|
||||||
return twofa, nil
|
return twofa, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasTwoFactorByUID returns the two-factor authentication token associated with
|
||||||
|
// the user, if any.
|
||||||
|
func HasTwoFactorByUID(uid int64) (bool, error) {
|
||||||
|
return db.GetEngine(db.DefaultContext).Where("uid=?", uid).Exist(&TwoFactor{})
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteTwoFactorByID deletes two-factor authentication token by given ID.
|
// DeleteTwoFactorByID deletes two-factor authentication token by given ID.
|
||||||
func DeleteTwoFactorByID(id, userID int64) error {
|
func DeleteTwoFactorByID(id, userID int64) error {
|
||||||
cnt, err := db.GetEngine(db.DefaultContext).ID(id).Delete(&TwoFactor{
|
cnt, err := db.GetEngine(db.DefaultContext).ID(id).Delete(&TwoFactor{
|
||||||
|
|
|
@ -115,6 +115,11 @@ func GetU2FRegistrationsByUID(uid int64) (U2FRegistrationList, error) {
|
||||||
return getU2FRegistrationsByUID(db.GetEngine(db.DefaultContext), uid)
|
return getU2FRegistrationsByUID(db.GetEngine(db.DefaultContext), uid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasU2FRegistrationsByUID returns whether a given user has U2F registrations
|
||||||
|
func HasU2FRegistrationsByUID(uid int64) (bool, error) {
|
||||||
|
return db.GetEngine(db.DefaultContext).Where("user_id = ?", uid).Exist(&U2FRegistration{})
|
||||||
|
}
|
||||||
|
|
||||||
func createRegistration(e db.Engine, userID int64, name string, reg *u2f.Registration) (*U2FRegistration, error) {
|
func createRegistration(e db.Engine, userID int64, name string, reg *u2f.Registration) (*U2FRegistration, error) {
|
||||||
raw, err := reg.MarshalBinary()
|
raw, err := reg.MarshalBinary()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -29,7 +29,7 @@ func TestGetU2FRegistrationByID(t *testing.T) {
|
||||||
func TestGetU2FRegistrationsByUID(t *testing.T) {
|
func TestGetU2FRegistrationsByUID(t *testing.T) {
|
||||||
assert.NoError(t, db.PrepareTestDatabase())
|
assert.NoError(t, db.PrepareTestDatabase())
|
||||||
|
|
||||||
res, err := GetU2FRegistrationsByUID(1)
|
res, err := GetU2FRegistrationsByUID(32)
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, res, 1)
|
assert.Len(t, res, 1)
|
||||||
|
|
|
@ -147,13 +147,13 @@ func TestSearchUsers(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
testUserSuccess(&SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}},
|
testUserSuccess(&SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}},
|
||||||
[]int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30})
|
[]int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32})
|
||||||
|
|
||||||
testUserSuccess(&SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolFalse},
|
testUserSuccess(&SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolFalse},
|
||||||
[]int64{9})
|
[]int64{9})
|
||||||
|
|
||||||
testUserSuccess(&SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolTrue},
|
testUserSuccess(&SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolTrue},
|
||||||
[]int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 28, 29, 30})
|
[]int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 28, 29, 30, 32})
|
||||||
|
|
||||||
testUserSuccess(&SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolTrue},
|
testUserSuccess(&SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolTrue},
|
||||||
[]int64{1, 10, 11, 12, 13, 14, 15, 16, 18})
|
[]int64{1, 10, 11, 12, 13, 14, 15, 16, 18})
|
||||||
|
|
|
@ -714,7 +714,6 @@ twofa_enrolled = Your account has been enrolled into two-factor authentication.
|
||||||
twofa_failed_get_secret = Failed to get secret.
|
twofa_failed_get_secret = Failed to get secret.
|
||||||
|
|
||||||
u2f_desc = Security keys are hardware devices containing cryptographic keys. They can be used for two-factor authentication. Security keys must support the <a rel="noreferrer" href="https://fidoalliance.org/">FIDO U2F</a> standard.
|
u2f_desc = Security keys are hardware devices containing cryptographic keys. They can be used for two-factor authentication. Security keys must support the <a rel="noreferrer" href="https://fidoalliance.org/">FIDO U2F</a> standard.
|
||||||
u2f_require_twofa = Your account must be enrolled in two-factor authentication to use security keys.
|
|
||||||
u2f_register_key = Add Security Key
|
u2f_register_key = Add Security Key
|
||||||
u2f_nickname = Nickname
|
u2f_nickname = Nickname
|
||||||
u2f_press_button = Press the button on your security key to register it.
|
u2f_press_button = Press the button on your security key to register it.
|
||||||
|
|
|
@ -211,38 +211,58 @@ func SignInPost(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this user is enrolled in 2FA, we can't sign the user in just yet.
|
// If this user is enrolled in 2FA TOTP, we can't sign the user in just yet.
|
||||||
// Instead, redirect them to the 2FA authentication page.
|
// Instead, redirect them to the 2FA authentication page.
|
||||||
_, err = login.GetTwoFactorByUID(u.ID)
|
hasTOTPtwofa, err := login.HasTwoFactorByUID(u.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if login.IsErrTwoFactorNotEnrolled(err) {
|
ctx.ServerError("UserSignIn", err)
|
||||||
handleSignIn(ctx, u, form.Remember)
|
|
||||||
} else {
|
|
||||||
ctx.ServerError("UserSignIn", err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// User needs to use 2FA, save data and redirect to 2FA page.
|
// Check if the user has u2f registration
|
||||||
|
hasU2Ftwofa, err := login.HasU2FRegistrationsByUID(u.ID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("UserSignIn", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasTOTPtwofa && !hasU2Ftwofa {
|
||||||
|
// No two factor auth configured we can sign in the user
|
||||||
|
handleSignIn(ctx, u, form.Remember)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// User will need to use 2FA TOTP or U2F, save data
|
||||||
if err := ctx.Session.Set("twofaUid", u.ID); err != nil {
|
if err := ctx.Session.Set("twofaUid", u.ID); err != nil {
|
||||||
ctx.ServerError("UserSignIn: Unable to set twofaUid in session", err)
|
ctx.ServerError("UserSignIn: Unable to set twofaUid in session", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ctx.Session.Set("twofaRemember", form.Remember); err != nil {
|
if err := ctx.Session.Set("twofaRemember", form.Remember); err != nil {
|
||||||
ctx.ServerError("UserSignIn: Unable to set twofaRemember in session", err)
|
ctx.ServerError("UserSignIn: Unable to set twofaRemember in session", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if hasTOTPtwofa {
|
||||||
|
// User will need to use U2F, save data
|
||||||
|
if err := ctx.Session.Set("totpEnrolled", u.ID); err != nil {
|
||||||
|
ctx.ServerError("UserSignIn: Unable to set u2fEnrolled in session", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := ctx.Session.Release(); err != nil {
|
if err := ctx.Session.Release(); err != nil {
|
||||||
ctx.ServerError("UserSignIn: Unable to save session", err)
|
ctx.ServerError("UserSignIn: Unable to save session", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
regs, err := login.GetU2FRegistrationsByUID(u.ID)
|
// If we have U2F redirect there first
|
||||||
if err == nil && len(regs) > 0 {
|
if hasU2Ftwofa {
|
||||||
ctx.Redirect(setting.AppSubURL + "/user/u2f")
|
ctx.Redirect(setting.AppSubURL + "/user/u2f")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback to 2FA
|
||||||
ctx.Redirect(setting.AppSubURL + "/user/two_factor")
|
ctx.Redirect(setting.AppSubURL + "/user/two_factor")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -406,6 +426,11 @@ func U2F(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// See whether TOTP is also available.
|
||||||
|
if ctx.Session.Get("totpEnrolled") != nil {
|
||||||
|
ctx.Data["TOTPEnrolled"] = true
|
||||||
|
}
|
||||||
|
|
||||||
ctx.HTML(http.StatusOK, tplU2F)
|
ctx.HTML(http.StatusOK, tplU2F)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,23 +55,17 @@ func DeleteAccountLink(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadSecurityData(ctx *context.Context) {
|
func loadSecurityData(ctx *context.Context) {
|
||||||
enrolled := true
|
enrolled, err := login.HasTwoFactorByUID(ctx.User.ID)
|
||||||
_, err := login.GetTwoFactorByUID(ctx.User.ID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if login.IsErrTwoFactorNotEnrolled(err) {
|
ctx.ServerError("SettingsTwoFactor", err)
|
||||||
enrolled = false
|
return
|
||||||
} else {
|
|
||||||
ctx.ServerError("SettingsTwoFactor", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ctx.Data["TwofaEnrolled"] = enrolled
|
ctx.Data["TOTPEnrolled"] = enrolled
|
||||||
if enrolled {
|
|
||||||
ctx.Data["U2FRegistrations"], err = login.GetU2FRegistrationsByUID(ctx.User.ID)
|
ctx.Data["U2FRegistrations"], err = login.GetU2FRegistrationsByUID(ctx.User.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetU2FRegistrationsByUID", err)
|
ctx.ServerError("GetU2FRegistrationsByUID", err)
|
||||||
return
|
return
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tokens, err := models.ListAccessTokens(models.ListAccessTokensOptions{UserID: ctx.User.ID})
|
tokens, err := models.ListAccessTokens(models.ListAccessTokensOptions{UserID: ctx.User.ID})
|
||||||
|
|
|
@ -12,9 +12,11 @@
|
||||||
<p>{{.i18n.Tr "u2f_sign_in"}}</p>
|
<p>{{.i18n.Tr "u2f_sign_in"}}</p>
|
||||||
</div>
|
</div>
|
||||||
<div id="wait-for-key" class="ui attached segment"><div class="ui active indeterminate inline loader"></div> {{.i18n.Tr "u2f_press_button"}} </div>
|
<div id="wait-for-key" class="ui attached segment"><div class="ui active indeterminate inline loader"></div> {{.i18n.Tr "u2f_press_button"}} </div>
|
||||||
<div class="ui attached segment">
|
{{if .TOTPEnrolled}}
|
||||||
<a href="{{AppSubUrl}}/user/two_factor">{{.i18n.Tr "u2f_use_twofa"}}</a>
|
<div class="ui attached segment">
|
||||||
</div>
|
<a href="{{AppSubUrl}}/user/two_factor">{{.i18n.Tr "u2f_use_twofa"}}</a>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
</h4>
|
</h4>
|
||||||
<div class="ui attached segment">
|
<div class="ui attached segment">
|
||||||
<p>{{.i18n.Tr "settings.twofa_desc"}}</p>
|
<p>{{.i18n.Tr "settings.twofa_desc"}}</p>
|
||||||
{{if .TwofaEnrolled}}
|
{{if .TOTPEnrolled}}
|
||||||
<p>{{$.i18n.Tr "settings.twofa_is_enrolled" | Str2html }}</p>
|
<p>{{$.i18n.Tr "settings.twofa_is_enrolled" | Str2html }}</p>
|
||||||
<form class="ui form" action="{{AppSubUrl}}/user/settings/security/two_factor/regenerate_scratch" method="post" enctype="multipart/form-data">
|
<form class="ui form" action="{{AppSubUrl}}/user/settings/security/two_factor/regenerate_scratch" method="post" enctype="multipart/form-data">
|
||||||
{{.CsrfTokenHtml}}
|
{{.CsrfTokenHtml}}
|
||||||
|
|
|
@ -3,32 +3,28 @@
|
||||||
</h4>
|
</h4>
|
||||||
<div class="ui attached segment">
|
<div class="ui attached segment">
|
||||||
<p>{{.i18n.Tr "settings.u2f_desc" | Str2html}}</p>
|
<p>{{.i18n.Tr "settings.u2f_desc" | Str2html}}</p>
|
||||||
{{if .TwofaEnrolled}}
|
<div class="ui key list">
|
||||||
<div class="ui key list">
|
{{range .U2FRegistrations}}
|
||||||
{{range .U2FRegistrations}}
|
<div class="item">
|
||||||
<div class="item">
|
<div class="right floated content">
|
||||||
<div class="right floated content">
|
<button class="ui red tiny button delete-button" id="delete-registration" data-url="{{$.Link}}/u2f/delete" data-id="{{.ID}}">
|
||||||
<button class="ui red tiny button delete-button" data-modal-id="delete-registration" data-url="{{$.Link}}/u2f/delete" data-id="{{.ID}}">
|
{{$.i18n.Tr "settings.delete_key"}}
|
||||||
{{$.i18n.Tr "settings.delete_key"}}
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
<div class="content">
|
||||||
<div class="content">
|
<strong>{{.Name}}</strong>
|
||||||
<strong>{{.Name}}</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
<div class="ui form">
|
|
||||||
{{.CsrfTokenHtml}}
|
|
||||||
<div class="required field">
|
|
||||||
<label for="nickname">{{.i18n.Tr "settings.u2f_nickname"}}</label>
|
|
||||||
<input id="nickname" name="nickname" type="text" required>
|
|
||||||
</div>
|
</div>
|
||||||
<button id="register-security-key" class="ui green button">{{svg "octicon-key"}} {{.i18n.Tr "settings.u2f_register_key"}}</button>
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<div class="ui form">
|
||||||
|
{{.CsrfTokenHtml}}
|
||||||
|
<div class="required field">
|
||||||
|
<label for="nickname">{{.i18n.Tr "settings.u2f_nickname"}}</label>
|
||||||
|
<input id="nickname" name="nickname" type="text" required>
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
<button id="register-security-key" class="ui green button">{{svg "octicon-key"}} {{.i18n.Tr "settings.u2f_register_key"}}</button>
|
||||||
<b>{{.i18n.Tr "settings.u2f_require_twofa"}}</b>
|
</div>
|
||||||
{{end}}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ui small modal" id="register-device">
|
<div class="ui small modal" id="register-device">
|
||||||
|
|
Loading…
Reference in a new issue