Fix: passkey login not working anymore (#32623)

Quick fix #32595, use authenticator auth flags to login

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
hiifong 2024-11-27 00:04:17 +08:00 committed by GitHub
parent 0f4b0cf892
commit 87bb5ed0bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 86 additions and 47 deletions

View File

@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
)
@ -89,14 +90,33 @@ func (cred *WebAuthnCredential) AfterLoad() {
// WebAuthnCredentialList is a list of *WebAuthnCredential
type WebAuthnCredentialList []*WebAuthnCredential
// newCredentialFlagsFromAuthenticatorFlags is copied from https://github.com/go-webauthn/webauthn/pull/337
// to convert protocol.AuthenticatorFlags to webauthn.CredentialFlags
func newCredentialFlagsFromAuthenticatorFlags(flags protocol.AuthenticatorFlags) webauthn.CredentialFlags {
return webauthn.CredentialFlags{
UserPresent: flags.HasUserPresent(),
UserVerified: flags.HasUserVerified(),
BackupEligible: flags.HasBackupEligible(),
BackupState: flags.HasBackupState(),
}
}
// ToCredentials will convert all WebAuthnCredentials to webauthn.Credentials
func (list WebAuthnCredentialList) ToCredentials() []webauthn.Credential {
func (list WebAuthnCredentialList) ToCredentials(defaultAuthFlags ...protocol.AuthenticatorFlags) []webauthn.Credential {
// TODO: at the moment, Gitea doesn't store or check the flags
// so we need to use the default flags from the authenticator to make the login validation pass
// In the future, we should:
// 1. store the flags when registering the credential
// 2. provide the stored flags when converting the credentials (for login)
// 3. for old users, still use this fallback to the default flags
defAuthFlags := util.OptionalArg(defaultAuthFlags)
creds := make([]webauthn.Credential, 0, len(list))
for _, cred := range list {
creds = append(creds, webauthn.Credential{
ID: cred.CredentialID,
PublicKey: cred.PublicKey,
AttestationType: cred.AttestationType,
Flags: newCredentialFlagsFromAuthenticatorFlags(defAuthFlags),
Authenticator: webauthn.Authenticator{
AAGUID: cred.AAGUID,
SignCount: cred.SignCount,

View File

@ -134,6 +134,9 @@ func SyncAllTables() error {
func InitEngine(ctx context.Context) error {
xormEngine, err := newXORMEngine()
if err != nil {
if strings.Contains(err.Error(), "SQLite3 support") {
return fmt.Errorf(`sqlite3 requires: -tags sqlite,sqlite_unlock_notify%s%w`, "\n", err)
}
return fmt.Errorf("failed to connect to database: %w", err)
}

View File

@ -18,7 +18,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/testlogger"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"xorm.io/xorm"
)
@ -33,15 +33,15 @@ func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, fu
ourSkip := 2
ourSkip += skip
deferFn := testlogger.PrintCurrentTest(t, ourSkip)
assert.NoError(t, unittest.SyncDirs(filepath.Join(filepath.Dir(setting.AppPath), "tests/gitea-repositories-meta"), setting.RepoRootPath))
require.NoError(t, unittest.SyncDirs(filepath.Join(filepath.Dir(setting.AppPath), "tests/gitea-repositories-meta"), setting.RepoRootPath))
if err := deleteDB(); err != nil {
t.Errorf("unable to reset database: %v", err)
t.Fatalf("unable to reset database: %v", err)
return nil, deferFn
}
x, err := newXORMEngine()
assert.NoError(t, err)
require.NoError(t, err)
if x != nil {
oldDefer := deferFn
deferFn = func() {

View File

@ -4,13 +4,14 @@
package webauthn
import (
"context"
"encoding/binary"
"encoding/gob"
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
@ -38,40 +39,42 @@ func Init() {
}
}
// User represents an implementation of webauthn.User based on User model
type User user_model.User
// user represents an implementation of webauthn.User based on User model
type user struct {
ctx context.Context
User *user_model.User
defaultAuthFlags protocol.AuthenticatorFlags
}
var _ webauthn.User = (*user)(nil)
func NewWebAuthnUser(ctx context.Context, u *user_model.User, defaultAuthFlags ...protocol.AuthenticatorFlags) webauthn.User {
return &user{ctx: ctx, User: u, defaultAuthFlags: util.OptionalArg(defaultAuthFlags)}
}
// WebAuthnID implements the webauthn.User interface
func (u *User) WebAuthnID() []byte {
func (u *user) WebAuthnID() []byte {
id := make([]byte, 8)
binary.PutVarint(id, u.ID)
binary.PutVarint(id, u.User.ID)
return id
}
// WebAuthnName implements the webauthn.User interface
func (u *User) WebAuthnName() string {
if u.LoginName == "" {
return u.Name
}
return u.LoginName
func (u *user) WebAuthnName() string {
return util.IfZero(u.User.LoginName, u.User.Name)
}
// WebAuthnDisplayName implements the webauthn.User interface
func (u *User) WebAuthnDisplayName() string {
return (*user_model.User)(u).DisplayName()
}
// WebAuthnIcon implements the webauthn.User interface
func (u *User) WebAuthnIcon() string {
return (*user_model.User)(u).AvatarLink(db.DefaultContext)
func (u *user) WebAuthnDisplayName() string {
return u.User.DisplayName()
}
// WebAuthnCredentials implements the webauthn.User interface
func (u *User) WebAuthnCredentials() []webauthn.Credential {
dbCreds, err := auth.GetWebAuthnCredentialsByUID(db.DefaultContext, u.ID)
func (u *user) WebAuthnCredentials() []webauthn.Credential {
dbCreds, err := auth.GetWebAuthnCredentialsByUID(u.ctx, u.User.ID)
if err != nil {
return nil
}
return dbCreds.ToCredentials()
return dbCreds.ToCredentials(u.defaultAuthFlags)
}

View File

@ -76,8 +76,17 @@ func WebAuthnPasskeyLogin(ctx *context.Context) {
}()
// Validate the parsed response.
// ParseCredentialRequestResponse+ValidateDiscoverableLogin equals to FinishDiscoverableLogin, but we need to ParseCredentialRequestResponse first to get flags
var user *user_model.User
cred, err := wa.WebAuthn.FinishDiscoverableLogin(func(rawID, userHandle []byte) (webauthn.User, error) {
parsedResponse, err := protocol.ParseCredentialRequestResponse(ctx.Req)
if err != nil {
// Failed authentication attempt.
log.Info("Failed authentication attempt for %s from %s: %v", user.Name, ctx.RemoteAddr(), err)
ctx.Status(http.StatusForbidden)
return
}
cred, err := wa.WebAuthn.ValidateDiscoverableLogin(func(rawID, userHandle []byte) (webauthn.User, error) {
userID, n := binary.Varint(userHandle)
if n <= 0 {
return nil, errors.New("invalid rawID")
@ -89,8 +98,8 @@ func WebAuthnPasskeyLogin(ctx *context.Context) {
return nil, err
}
return (*wa.User)(user), nil
}, *sessionData, ctx.Req)
return wa.NewWebAuthnUser(ctx, user, parsedResponse.Response.AuthenticatorData.Flags), nil
}, *sessionData, parsedResponse)
if err != nil {
// Failed authentication attempt.
log.Info("Failed authentication attempt for passkey from %s: %v", ctx.RemoteAddr(), err)
@ -171,7 +180,8 @@ func WebAuthnLoginAssertion(ctx *context.Context) {
return
}
assertion, sessionData, err := wa.WebAuthn.BeginLogin((*wa.User)(user))
webAuthnUser := wa.NewWebAuthnUser(ctx, user)
assertion, sessionData, err := wa.WebAuthn.BeginLogin(webAuthnUser)
if err != nil {
ctx.ServerError("webauthn.BeginLogin", err)
return
@ -216,7 +226,8 @@ func WebAuthnLoginAssertionPost(ctx *context.Context) {
}
// Validate the parsed response.
cred, err := wa.WebAuthn.ValidateLogin((*wa.User)(user), *sessionData, parsedResponse)
webAuthnUser := wa.NewWebAuthnUser(ctx, user, parsedResponse.Response.AuthenticatorData.Flags)
cred, err := wa.WebAuthn.ValidateLogin(webAuthnUser, *sessionData, parsedResponse)
if err != nil {
// Failed authentication attempt.
log.Info("Failed authentication attempt for %s from %s: %v", user.Name, ctx.RemoteAddr(), err)

View File

@ -51,7 +51,8 @@ func WebAuthnRegister(ctx *context.Context) {
return
}
credentialOptions, sessionData, err := wa.WebAuthn.BeginRegistration((*wa.User)(ctx.Doer), webauthn.WithAuthenticatorSelection(protocol.AuthenticatorSelection{
webAuthnUser := wa.NewWebAuthnUser(ctx, ctx.Doer)
credentialOptions, sessionData, err := wa.WebAuthn.BeginRegistration(webAuthnUser, webauthn.WithAuthenticatorSelection(protocol.AuthenticatorSelection{
ResidentKey: protocol.ResidentKeyRequirementRequired,
}))
if err != nil {
@ -92,7 +93,8 @@ func WebauthnRegisterPost(ctx *context.Context) {
}()
// Verify that the challenge succeeded
cred, err := wa.WebAuthn.FinishRegistration((*wa.User)(ctx.Doer), *sessionData, ctx.Req)
webAuthnUser := wa.NewWebAuthnUser(ctx, ctx.Doer)
cred, err := wa.WebAuthn.FinishRegistration(webAuthnUser, *sessionData, ctx.Req)
if err != nil {
if pErr, ok := err.(*protocol.Error); ok {
log.Error("Unable to finish registration due to error: %v\nDevInfo: %s", pErr, pErr.DevInfo)

View File

@ -40,14 +40,15 @@ async function loginPasskey() {
try {
const credential = await navigator.credentials.get({
publicKey: options.publicKey,
});
}) as PublicKeyCredential;
const credResp = credential.response as AuthenticatorAssertionResponse;
// Move data into Arrays in case it is super long
const authData = new Uint8Array(credential.response.authenticatorData);
const clientDataJSON = new Uint8Array(credential.response.clientDataJSON);
const authData = new Uint8Array(credResp.authenticatorData);
const clientDataJSON = new Uint8Array(credResp.clientDataJSON);
const rawId = new Uint8Array(credential.rawId);
const sig = new Uint8Array(credential.response.signature);
const userHandle = new Uint8Array(credential.response.userHandle);
const sig = new Uint8Array(credResp.signature);
const userHandle = new Uint8Array(credResp.userHandle);
const res = await POST(`${appSubUrl}/user/webauthn/passkey/login`, {
data: {
@ -175,7 +176,7 @@ async function webauthnRegistered(newCredential) {
window.location.reload();
}
function webAuthnError(errorType, message) {
function webAuthnError(errorType: string, message:string = '') {
const elErrorMsg = document.querySelector(`#webauthn-error-msg`);
if (errorType === 'general') {
@ -207,10 +208,9 @@ function detectWebAuthnSupport() {
}
export function initUserAuthWebAuthnRegister() {
const elRegister = document.querySelector('#register-webauthn');
if (!elRegister) {
return;
}
const elRegister = document.querySelector<HTMLInputElement>('#register-webauthn');
if (!elRegister) return;
if (!detectWebAuthnSupport()) {
elRegister.disabled = true;
return;
@ -222,7 +222,7 @@ export function initUserAuthWebAuthnRegister() {
}
async function webAuthnRegisterRequest() {
const elNickname = document.querySelector('#nickname');
const elNickname = document.querySelector<HTMLInputElement>('#nickname');
const formData = new FormData();
formData.append('name', elNickname.value);

View File

@ -1,5 +1,5 @@
import {isObject} from '../utils.ts';
import type {RequestData, RequestOpts} from '../types.ts';
import type {RequestOpts} from '../types.ts';
const {csrfToken} = window.config;
@ -10,7 +10,7 @@ const safeMethods = new Set(['GET', 'HEAD', 'OPTIONS', 'TRACE']);
// which will automatically set an appropriate headers. For json content, only object
// and array types are currently supported.
export function request(url: string, {method = 'GET', data, headers = {}, ...other}: RequestOpts = {}): Promise<Response> {
let body: RequestData;
let body: string | FormData | URLSearchParams;
let contentType: string;
if (data instanceof FormData || data instanceof URLSearchParams) {
body = data;

View File

@ -24,7 +24,7 @@ export type Config = {
export type Intent = 'error' | 'warning' | 'info';
export type RequestData = string | FormData | URLSearchParams;
export type RequestData = string | FormData | URLSearchParams | Record<string, any>;
export type RequestOpts = {
data?: RequestData,