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:
parent
0f4b0cf892
commit
87bb5ed0bc
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user