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
					
				| @ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user