Webauthn nits (#18284)

This contains some additional fixes and small nits related to #17957 

Signed-off-by: Andrew Thornton <art27@cantab.net>
Co-authored-by: 6543 <6543@obermui.de>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
zeripath 2022-01-15 16:52:56 +00:00 committed by GitHub
parent e239d354c9
commit d7c2a2951c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 87 additions and 40 deletions

View file

@ -6,7 +6,7 @@ package auth
import ( import (
"context" "context"
"encoding/base64" "encoding/base32"
"fmt" "fmt"
"strings" "strings"
@ -94,7 +94,7 @@ type WebAuthnCredentialList []*WebAuthnCredential
func (list WebAuthnCredentialList) ToCredentials() []webauthn.Credential { func (list WebAuthnCredentialList) ToCredentials() []webauthn.Credential {
creds := make([]webauthn.Credential, 0, len(list)) creds := make([]webauthn.Credential, 0, len(list))
for _, cred := range list { for _, cred := range list {
credID, _ := base64.RawStdEncoding.DecodeString(cred.CredentialID) credID, _ := base32.HexEncoding.DecodeString(cred.CredentialID)
creds = append(creds, webauthn.Credential{ creds = append(creds, webauthn.Credential{
ID: credID, ID: credID,
PublicKey: cred.PublicKey, PublicKey: cred.PublicKey,
@ -164,13 +164,13 @@ func HasWebAuthnRegistrationsByUID(uid int64) (bool, error) {
} }
// GetWebAuthnCredentialByCredID returns WebAuthn credential by credential ID // GetWebAuthnCredentialByCredID returns WebAuthn credential by credential ID
func GetWebAuthnCredentialByCredID(credID string) (*WebAuthnCredential, error) { func GetWebAuthnCredentialByCredID(userID int64, credID string) (*WebAuthnCredential, error) {
return getWebAuthnCredentialByCredID(db.DefaultContext, credID) return getWebAuthnCredentialByCredID(db.DefaultContext, userID, credID)
} }
func getWebAuthnCredentialByCredID(ctx context.Context, credID string) (*WebAuthnCredential, error) { func getWebAuthnCredentialByCredID(ctx context.Context, userID int64, credID string) (*WebAuthnCredential, error) {
cred := new(WebAuthnCredential) cred := new(WebAuthnCredential)
if found, err := db.GetEngine(ctx).Where("credential_id = ?", credID).Get(cred); err != nil { if found, err := db.GetEngine(ctx).Where("user_id = ? AND credential_id = ?", userID, credID).Get(cred); err != nil {
return nil, err return nil, err
} else if !found { } else if !found {
return nil, ErrWebAuthnCredentialNotExist{CredentialID: credID} return nil, ErrWebAuthnCredentialNotExist{CredentialID: credID}
@ -187,7 +187,7 @@ func createCredential(ctx context.Context, userID int64, name string, cred *weba
c := &WebAuthnCredential{ c := &WebAuthnCredential{
UserID: userID, UserID: userID,
Name: name, Name: name,
CredentialID: base64.RawStdEncoding.EncodeToString(cred.ID), CredentialID: base32.HexEncoding.EncodeToString(cred.ID),
PublicKey: cred.PublicKey, PublicKey: cred.PublicKey,
AttestationType: cred.AttestationType, AttestationType: cred.AttestationType,
AAGUID: cred.Authenticator.AAGUID, AAGUID: cred.Authenticator.AAGUID,

View file

@ -5,7 +5,7 @@
package auth package auth
import ( import (
"encoding/base64" "encoding/base32"
"testing" "testing"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
@ -61,7 +61,7 @@ func TestCreateCredential(t *testing.T) {
res, err := CreateCredential(1, "WebAuthn Created Credential", &webauthn.Credential{ID: []byte("Test")}) res, err := CreateCredential(1, "WebAuthn Created Credential", &webauthn.Credential{ID: []byte("Test")})
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "WebAuthn Created Credential", res.Name) assert.Equal(t, "WebAuthn Created Credential", res.Name)
bs, err := base64.RawStdEncoding.DecodeString(res.CredentialID) bs, err := base32.HexEncoding.DecodeString(res.CredentialID)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, []byte("Test"), bs) assert.Equal(t, []byte("Test"), bs)

View file

@ -368,6 +368,8 @@ var migrations = []Migration{
NewMigration("Add authorize column to team_unit table", addAuthorizeColForTeamUnit), NewMigration("Add authorize column to team_unit table", addAuthorizeColForTeamUnit),
// v207 -> v208 // v207 -> v208
NewMigration("Add webauthn table and migrate u2f data to webauthn", addWebAuthnCred), NewMigration("Add webauthn table and migrate u2f data to webauthn", addWebAuthnCred),
// v208 -> v209
NewMigration("Use base32.HexEncoding instead of base64 encoding for cred ID as it is case insensitive", useBase32HexForCredIDInWebAuthnCredential),
} }
// GetCurrentDBVersion returns the current db version // GetCurrentDBVersion returns the current db version

51
models/migrations/v208.go Normal file
View file

@ -0,0 +1,51 @@
// Copyright 2021 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 migrations
import (
"encoding/base32"
"encoding/base64"
"xorm.io/xorm"
)
func useBase32HexForCredIDInWebAuthnCredential(x *xorm.Engine) error {
// Create webauthnCredential table
type webauthnCredential struct {
ID int64 `xorm:"pk autoincr"`
CredentialID string `xorm:"INDEX"`
}
if err := x.Sync2(&webauthnCredential{}); err != nil {
return err
}
var start int
regs := make([]*webauthnCredential, 0, 50)
for {
err := x.OrderBy("id").Limit(50, start).Find(&regs)
if err != nil {
return err
}
for _, reg := range regs {
credID, _ := base64.RawStdEncoding.DecodeString(reg.CredentialID)
reg.CredentialID = base32.HexEncoding.EncodeToString(credID)
_, err := x.Update(reg)
if err != nil {
return err
}
}
if len(regs) < 50 {
break
}
start += 50
regs = regs[:0]
}
return nil
}

View file

@ -748,10 +748,9 @@ 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! 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!
twofa_failed_get_secret = Failed to get secret. twofa_failed_get_secret = Failed to get secret.
webauthn_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://w3c.github.io/webauthn/#webauthn-authenticator">WebAuthn Authenticator</a> standard. webauthn_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" target="_blank" href="https://w3c.github.io/webauthn/#webauthn-authenticator">WebAuthn Authenticator</a> standard.
webauthn_register_key = Add Security Key webauthn_register_key = Add Security Key
webauthn_nickname = Nickname webauthn_nickname = Nickname
webauthn_press_button = Press the button on your security key to register it.
webauthn_delete_key = Remove Security Key webauthn_delete_key = Remove Security Key
webauthn_delete_key_desc = If you remove a security key you can no longer sign in with it. Continue? webauthn_delete_key_desc = If you remove a security key you can no longer sign in with it. Continue?

View file

@ -5,7 +5,7 @@
package auth package auth
import ( import (
"encoding/base64" "encoding/base32"
"errors" "errors"
"net/http" "net/http"
@ -131,7 +131,7 @@ func WebAuthnLoginAssertionPost(ctx *context.Context) {
} }
// Success! Get the credential and update the sign count with the new value we received. // Success! Get the credential and update the sign count with the new value we received.
dbCred, err := auth.GetWebAuthnCredentialByCredID(base64.RawStdEncoding.EncodeToString(cred.ID)) dbCred, err := auth.GetWebAuthnCredentialByCredID(user.ID, base32.HexEncoding.EncodeToString(cred.ID))
if err != nil { if err != nil {
ctx.ServerError("GetWebAuthnCredentialByCredID", err) ctx.ServerError("GetWebAuthnCredentialByCredID", err)
return return

View file

@ -38,9 +38,9 @@ func WebAuthnRegister(ctx *context.Context) {
return return
} }
_ = ctx.Session.Delete("registration") _ = ctx.Session.Delete("webauthnRegistration")
if err := ctx.Session.Set("WebauthnName", form.Name); err != nil { if err := ctx.Session.Set("webauthnName", form.Name); err != nil {
ctx.ServerError("Unable to set session key for WebauthnName", err) ctx.ServerError("Unable to set session key for webauthnName", err)
return return
} }
@ -51,7 +51,7 @@ func WebAuthnRegister(ctx *context.Context) {
} }
// Save the session data as marshaled JSON // Save the session data as marshaled JSON
if err = ctx.Session.Set("registration", sessionData); err != nil { if err = ctx.Session.Set("webauthnRegistration", sessionData); err != nil {
ctx.ServerError("Unable to set session", err) ctx.ServerError("Unable to set session", err)
return return
} }
@ -61,20 +61,20 @@ func WebAuthnRegister(ctx *context.Context) {
// WebauthnRegisterPost receives the response of the security key // WebauthnRegisterPost receives the response of the security key
func WebauthnRegisterPost(ctx *context.Context) { func WebauthnRegisterPost(ctx *context.Context) {
name, ok := ctx.Session.Get("WebauthnName").(string) name, ok := ctx.Session.Get("webauthnName").(string)
if !ok || name == "" { if !ok || name == "" {
ctx.ServerError("Get WebauthnName", errors.New("no WebauthnName")) ctx.ServerError("Get webauthnName", errors.New("no webauthnName"))
return return
} }
// Load the session data // Load the session data
sessionData, ok := ctx.Session.Get("registration").(*webauthn.SessionData) sessionData, ok := ctx.Session.Get("webauthnRegistration").(*webauthn.SessionData)
if !ok || sessionData == nil { if !ok || sessionData == nil {
ctx.ServerError("Get registration", errors.New("no registration")) ctx.ServerError("Get registration", errors.New("no registration"))
return return
} }
defer func() { defer func() {
_ = ctx.Session.Delete("registration") _ = ctx.Session.Delete("webauthnRegistration")
}() }()
// Verify that the challenge succeeded // Verify that the challenge succeeded
@ -103,6 +103,8 @@ func WebauthnRegisterPost(ctx *context.Context) {
ctx.ServerError("CreateCredential", err) ctx.ServerError("CreateCredential", err)
return return
} }
_ = ctx.Session.Delete("webauthnName")
ctx.JSON(http.StatusCreated, cred) ctx.JSON(http.StatusCreated, cred)
} }

View file

@ -12,7 +12,7 @@
<div class="hide" data-webauthn-error-msg="duplicated"><p>{{.i18n.Tr "webauthn_error_duplicated"}}</div> <div class="hide" data-webauthn-error-msg="duplicated"><p>{{.i18n.Tr "webauthn_error_duplicated"}}</div>
<div class="hide" data-webauthn-error-msg="empty"><p>{{.i18n.Tr "webauthn_error_empty"}}</div> <div class="hide" data-webauthn-error-msg="empty"><p>{{.i18n.Tr "webauthn_error_empty"}}</div>
<div class="hide" data-webauthn-error-msg="timeout"><p>{{.i18n.Tr "webauthn_error_timeout"}}</div> <div class="hide" data-webauthn-error-msg="timeout"><p>{{.i18n.Tr "webauthn_error_timeout"}}</div>
<div class="hide" data-webauthn-error-msg="0"></div> <div class="hide" data-webauthn-error-msg="general"></div>
</div> </div>
</div> </div>
<div class="actions"> <div class="actions">

View file

@ -28,16 +28,6 @@
</div> </div>
</div> </div>
<div class="ui small modal" id="register-device">
<div class="header">{{.i18n.Tr "settings.webauthn_register_key"}}</div>
<div class="content">
<i class="notched spinner loading icon"></i> {{.i18n.Tr "settings.webauthn_press_button"}}
</div>
<div class="actions">
<div class="ui cancel button">{{.i18n.Tr "cancel"}}</div>
</div>
</div>
{{template "user/auth/webauthn_error" .}} {{template "user/auth/webauthn_error" .}}
<div class="ui small basic delete modal" id="delete-registration"> <div class="ui small basic delete modal" id="delete-registration">

View file

@ -24,7 +24,7 @@ export function initUserAuthWebAuthn() {
.then((credential) => { .then((credential) => {
verifyAssertion(credential); verifyAssertion(credential);
}).catch((err) => { }).catch((err) => {
webAuthnError(0, err.message); webAuthnError('general', err.message);
}); });
}).fail(() => { }).fail(() => {
webAuthnError('unknown'); webAuthnError('unknown');
@ -113,11 +113,16 @@ function webauthnRegistered(newCredential) {
function webAuthnError(errorType, message) { function webAuthnError(errorType, message) {
$('#webauthn-error [data-webauthn-error-msg]').hide(); $('#webauthn-error [data-webauthn-error-msg]').hide();
if (errorType === 0 && message && message.length > 1) { const $errorGeneral = $(`#webauthn-error [data-webauthn-error-msg=general]`);
$(`#webauthn-error [data-webauthn-error-msg=0]`).text(message); if (errorType === 'general') {
$(`#webauthn-error [data-webauthn-error-msg=0]`).show(); $errorGeneral.show().text(message || 'unknown error');
} else { } else {
$(`#webauthn-error [data-webauthn-error-msg=${errorType}]`).show(); const $errorTyped = $(`#webauthn-error [data-webauthn-error-msg=${errorType}]`);
if ($errorTyped.length) {
$errorTyped.show();
} else {
$errorGeneral.show().text(`unknown error type: ${errorType}`);
}
} }
$('#webauthn-error').modal('show'); $('#webauthn-error').modal('show');
} }
@ -149,7 +154,6 @@ export function initUserAuthWebAuthnRegister() {
return; return;
} }
$('#register-device').modal({allowMultiple: false});
$('#webauthn-error').modal({allowMultiple: false}); $('#webauthn-error').modal({allowMultiple: false});
$('#register-webauthn').on('click', (e) => { $('#register-webauthn').on('click', (e) => {
e.preventDefault(); e.preventDefault();
@ -167,7 +171,6 @@ function webAuthnRegisterRequest() {
name: $('#nickname').val(), name: $('#nickname').val(),
}).done((makeCredentialOptions) => { }).done((makeCredentialOptions) => {
$('#nickname').closest('div.field').removeClass('error'); $('#nickname').closest('div.field').removeClass('error');
$('#register-device').modal('show');
makeCredentialOptions.publicKey.challenge = decode(makeCredentialOptions.publicKey.challenge); makeCredentialOptions.publicKey.challenge = decode(makeCredentialOptions.publicKey.challenge);
makeCredentialOptions.publicKey.user.id = decode(makeCredentialOptions.publicKey.user.id); makeCredentialOptions.publicKey.user.id = decode(makeCredentialOptions.publicKey.user.id);
@ -185,7 +188,7 @@ function webAuthnRegisterRequest() {
webAuthnError('unknown'); webAuthnError('unknown');
return; return;
} }
webAuthnError(0, err); webAuthnError('general', err.message);
}); });
}).fail((xhr) => { }).fail((xhr) => {
if (xhr.status === 409) { if (xhr.status === 409) {