mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 19:06:18 +01:00 
			
		
		
		
	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:
		| @@ -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, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user