mirror of
https://github.com/go-gitea/gitea.git
synced 2024-12-28 16:36:53 +00: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:
parent
0f4b0cf892
commit
87bb5ed0bc
@ -12,6 +12,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
|
"github.com/go-webauthn/webauthn/protocol"
|
||||||
"github.com/go-webauthn/webauthn/webauthn"
|
"github.com/go-webauthn/webauthn/webauthn"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -89,14 +90,33 @@ func (cred *WebAuthnCredential) AfterLoad() {
|
|||||||
// WebAuthnCredentialList is a list of *WebAuthnCredential
|
// WebAuthnCredentialList is a list of *WebAuthnCredential
|
||||||
type WebAuthnCredentialList []*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
|
// 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))
|
creds := make([]webauthn.Credential, 0, len(list))
|
||||||
for _, cred := range list {
|
for _, cred := range list {
|
||||||
creds = append(creds, webauthn.Credential{
|
creds = append(creds, webauthn.Credential{
|
||||||
ID: cred.CredentialID,
|
ID: cred.CredentialID,
|
||||||
PublicKey: cred.PublicKey,
|
PublicKey: cred.PublicKey,
|
||||||
AttestationType: cred.AttestationType,
|
AttestationType: cred.AttestationType,
|
||||||
|
Flags: newCredentialFlagsFromAuthenticatorFlags(defAuthFlags),
|
||||||
Authenticator: webauthn.Authenticator{
|
Authenticator: webauthn.Authenticator{
|
||||||
AAGUID: cred.AAGUID,
|
AAGUID: cred.AAGUID,
|
||||||
SignCount: cred.SignCount,
|
SignCount: cred.SignCount,
|
||||||
|
@ -134,6 +134,9 @@ func SyncAllTables() error {
|
|||||||
func InitEngine(ctx context.Context) error {
|
func InitEngine(ctx context.Context) error {
|
||||||
xormEngine, err := newXORMEngine()
|
xormEngine, err := newXORMEngine()
|
||||||
if err != nil {
|
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)
|
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/setting"
|
||||||
"code.gitea.io/gitea/modules/testlogger"
|
"code.gitea.io/gitea/modules/testlogger"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/require"
|
||||||
"xorm.io/xorm"
|
"xorm.io/xorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -33,15 +33,15 @@ func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, fu
|
|||||||
ourSkip := 2
|
ourSkip := 2
|
||||||
ourSkip += skip
|
ourSkip += skip
|
||||||
deferFn := testlogger.PrintCurrentTest(t, ourSkip)
|
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 {
|
if err := deleteDB(); err != nil {
|
||||||
t.Errorf("unable to reset database: %v", err)
|
t.Fatalf("unable to reset database: %v", err)
|
||||||
return nil, deferFn
|
return nil, deferFn
|
||||||
}
|
}
|
||||||
|
|
||||||
x, err := newXORMEngine()
|
x, err := newXORMEngine()
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
if x != nil {
|
if x != nil {
|
||||||
oldDefer := deferFn
|
oldDefer := deferFn
|
||||||
deferFn = func() {
|
deferFn = func() {
|
||||||
|
@ -4,13 +4,14 @@
|
|||||||
package webauthn
|
package webauthn
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/auth"
|
"code.gitea.io/gitea/models/auth"
|
||||||
"code.gitea.io/gitea/models/db"
|
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"github.com/go-webauthn/webauthn/protocol"
|
"github.com/go-webauthn/webauthn/protocol"
|
||||||
"github.com/go-webauthn/webauthn/webauthn"
|
"github.com/go-webauthn/webauthn/webauthn"
|
||||||
@ -38,40 +39,42 @@ func Init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// User represents an implementation of webauthn.User based on User model
|
// user represents an implementation of webauthn.User based on User model
|
||||||
type User user_model.User
|
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
|
// WebAuthnID implements the webauthn.User interface
|
||||||
func (u *User) WebAuthnID() []byte {
|
func (u *user) WebAuthnID() []byte {
|
||||||
id := make([]byte, 8)
|
id := make([]byte, 8)
|
||||||
binary.PutVarint(id, u.ID)
|
binary.PutVarint(id, u.User.ID)
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebAuthnName implements the webauthn.User interface
|
// WebAuthnName implements the webauthn.User interface
|
||||||
func (u *User) WebAuthnName() string {
|
func (u *user) WebAuthnName() string {
|
||||||
if u.LoginName == "" {
|
return util.IfZero(u.User.LoginName, u.User.Name)
|
||||||
return u.Name
|
|
||||||
}
|
|
||||||
return u.LoginName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebAuthnDisplayName implements the webauthn.User interface
|
// WebAuthnDisplayName implements the webauthn.User interface
|
||||||
func (u *User) WebAuthnDisplayName() string {
|
func (u *user) WebAuthnDisplayName() string {
|
||||||
return (*user_model.User)(u).DisplayName()
|
return u.User.DisplayName()
|
||||||
}
|
|
||||||
|
|
||||||
// WebAuthnIcon implements the webauthn.User interface
|
|
||||||
func (u *User) WebAuthnIcon() string {
|
|
||||||
return (*user_model.User)(u).AvatarLink(db.DefaultContext)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebAuthnCredentials implements the webauthn.User interface
|
// WebAuthnCredentials implements the webauthn.User interface
|
||||||
func (u *User) WebAuthnCredentials() []webauthn.Credential {
|
func (u *user) WebAuthnCredentials() []webauthn.Credential {
|
||||||
dbCreds, err := auth.GetWebAuthnCredentialsByUID(db.DefaultContext, u.ID)
|
dbCreds, err := auth.GetWebAuthnCredentialsByUID(u.ctx, u.User.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
return dbCreds.ToCredentials(u.defaultAuthFlags)
|
||||||
return dbCreds.ToCredentials()
|
|
||||||
}
|
}
|
||||||
|
@ -76,8 +76,17 @@ func WebAuthnPasskeyLogin(ctx *context.Context) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// Validate the parsed response.
|
// Validate the parsed response.
|
||||||
|
|
||||||
|
// ParseCredentialRequestResponse+ValidateDiscoverableLogin equals to FinishDiscoverableLogin, but we need to ParseCredentialRequestResponse first to get flags
|
||||||
var user *user_model.User
|
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)
|
userID, n := binary.Varint(userHandle)
|
||||||
if n <= 0 {
|
if n <= 0 {
|
||||||
return nil, errors.New("invalid rawID")
|
return nil, errors.New("invalid rawID")
|
||||||
@ -89,8 +98,8 @@ func WebAuthnPasskeyLogin(ctx *context.Context) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return (*wa.User)(user), nil
|
return wa.NewWebAuthnUser(ctx, user, parsedResponse.Response.AuthenticatorData.Flags), nil
|
||||||
}, *sessionData, ctx.Req)
|
}, *sessionData, parsedResponse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Failed authentication attempt.
|
// Failed authentication attempt.
|
||||||
log.Info("Failed authentication attempt for passkey from %s: %v", ctx.RemoteAddr(), err)
|
log.Info("Failed authentication attempt for passkey from %s: %v", ctx.RemoteAddr(), err)
|
||||||
@ -171,7 +180,8 @@ func WebAuthnLoginAssertion(ctx *context.Context) {
|
|||||||
return
|
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 {
|
if err != nil {
|
||||||
ctx.ServerError("webauthn.BeginLogin", err)
|
ctx.ServerError("webauthn.BeginLogin", err)
|
||||||
return
|
return
|
||||||
@ -216,7 +226,8 @@ func WebAuthnLoginAssertionPost(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate the parsed response.
|
// 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 {
|
if err != nil {
|
||||||
// Failed authentication attempt.
|
// Failed authentication attempt.
|
||||||
log.Info("Failed authentication attempt for %s from %s: %v", user.Name, ctx.RemoteAddr(), err)
|
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
|
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,
|
ResidentKey: protocol.ResidentKeyRequirementRequired,
|
||||||
}))
|
}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -92,7 +93,8 @@ func WebauthnRegisterPost(ctx *context.Context) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// Verify that the challenge succeeded
|
// 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 err != nil {
|
||||||
if pErr, ok := err.(*protocol.Error); ok {
|
if pErr, ok := err.(*protocol.Error); ok {
|
||||||
log.Error("Unable to finish registration due to error: %v\nDevInfo: %s", pErr, pErr.DevInfo)
|
log.Error("Unable to finish registration due to error: %v\nDevInfo: %s", pErr, pErr.DevInfo)
|
||||||
|
@ -40,14 +40,15 @@ async function loginPasskey() {
|
|||||||
try {
|
try {
|
||||||
const credential = await navigator.credentials.get({
|
const credential = await navigator.credentials.get({
|
||||||
publicKey: options.publicKey,
|
publicKey: options.publicKey,
|
||||||
});
|
}) as PublicKeyCredential;
|
||||||
|
const credResp = credential.response as AuthenticatorAssertionResponse;
|
||||||
|
|
||||||
// Move data into Arrays in case it is super long
|
// Move data into Arrays in case it is super long
|
||||||
const authData = new Uint8Array(credential.response.authenticatorData);
|
const authData = new Uint8Array(credResp.authenticatorData);
|
||||||
const clientDataJSON = new Uint8Array(credential.response.clientDataJSON);
|
const clientDataJSON = new Uint8Array(credResp.clientDataJSON);
|
||||||
const rawId = new Uint8Array(credential.rawId);
|
const rawId = new Uint8Array(credential.rawId);
|
||||||
const sig = new Uint8Array(credential.response.signature);
|
const sig = new Uint8Array(credResp.signature);
|
||||||
const userHandle = new Uint8Array(credential.response.userHandle);
|
const userHandle = new Uint8Array(credResp.userHandle);
|
||||||
|
|
||||||
const res = await POST(`${appSubUrl}/user/webauthn/passkey/login`, {
|
const res = await POST(`${appSubUrl}/user/webauthn/passkey/login`, {
|
||||||
data: {
|
data: {
|
||||||
@ -175,7 +176,7 @@ async function webauthnRegistered(newCredential) {
|
|||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
function webAuthnError(errorType, message) {
|
function webAuthnError(errorType: string, message:string = '') {
|
||||||
const elErrorMsg = document.querySelector(`#webauthn-error-msg`);
|
const elErrorMsg = document.querySelector(`#webauthn-error-msg`);
|
||||||
|
|
||||||
if (errorType === 'general') {
|
if (errorType === 'general') {
|
||||||
@ -207,10 +208,9 @@ function detectWebAuthnSupport() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function initUserAuthWebAuthnRegister() {
|
export function initUserAuthWebAuthnRegister() {
|
||||||
const elRegister = document.querySelector('#register-webauthn');
|
const elRegister = document.querySelector<HTMLInputElement>('#register-webauthn');
|
||||||
if (!elRegister) {
|
if (!elRegister) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!detectWebAuthnSupport()) {
|
if (!detectWebAuthnSupport()) {
|
||||||
elRegister.disabled = true;
|
elRegister.disabled = true;
|
||||||
return;
|
return;
|
||||||
@ -222,7 +222,7 @@ export function initUserAuthWebAuthnRegister() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function webAuthnRegisterRequest() {
|
async function webAuthnRegisterRequest() {
|
||||||
const elNickname = document.querySelector('#nickname');
|
const elNickname = document.querySelector<HTMLInputElement>('#nickname');
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('name', elNickname.value);
|
formData.append('name', elNickname.value);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {isObject} from '../utils.ts';
|
import {isObject} from '../utils.ts';
|
||||||
import type {RequestData, RequestOpts} from '../types.ts';
|
import type {RequestOpts} from '../types.ts';
|
||||||
|
|
||||||
const {csrfToken} = window.config;
|
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
|
// which will automatically set an appropriate headers. For json content, only object
|
||||||
// and array types are currently supported.
|
// and array types are currently supported.
|
||||||
export function request(url: string, {method = 'GET', data, headers = {}, ...other}: RequestOpts = {}): Promise<Response> {
|
export function request(url: string, {method = 'GET', data, headers = {}, ...other}: RequestOpts = {}): Promise<Response> {
|
||||||
let body: RequestData;
|
let body: string | FormData | URLSearchParams;
|
||||||
let contentType: string;
|
let contentType: string;
|
||||||
if (data instanceof FormData || data instanceof URLSearchParams) {
|
if (data instanceof FormData || data instanceof URLSearchParams) {
|
||||||
body = data;
|
body = data;
|
||||||
|
@ -24,7 +24,7 @@ export type Config = {
|
|||||||
|
|
||||||
export type Intent = 'error' | 'warning' | 'info';
|
export type Intent = 'error' | 'warning' | 'info';
|
||||||
|
|
||||||
export type RequestData = string | FormData | URLSearchParams;
|
export type RequestData = string | FormData | URLSearchParams | Record<string, any>;
|
||||||
|
|
||||||
export type RequestOpts = {
|
export type RequestOpts = {
|
||||||
data?: RequestData,
|
data?: RequestData,
|
||||||
|
Loading…
Reference in New Issue
Block a user