feat: add back support for non-discoverable credential WebAuthn login and display WebAuthn ID again (#3998)

This commit is contained in:
DacongDA
2025-07-25 18:34:37 +08:00
committed by GitHub
parent 5f702ca418
commit fea6317430
4 changed files with 67 additions and 14 deletions

View File

@@ -17,6 +17,7 @@ package controllers
import (
"bytes"
"encoding/base64"
"fmt"
"io"
"github.com/casdoor/casdoor/form"
@@ -47,6 +48,13 @@ func (c *ApiController) WebAuthnSignupBegin() {
registerOptions := func(credCreationOpts *protocol.PublicKeyCredentialCreationOptions) {
credCreationOpts.CredentialExcludeList = user.CredentialExcludeList()
credCreationOpts.AuthenticatorSelection.ResidentKey = "preferred"
credCreationOpts.Attestation = "none"
ext := map[string]interface{}{
"credProps": true,
}
credCreationOpts.Extensions = ext
}
options, sessionData, err := webauthnObj.BeginRegistration(
user,
@@ -118,7 +126,34 @@ func (c *ApiController) WebAuthnSigninBegin() {
return
}
options, sessionData, err := webauthnObj.BeginDiscoverableLogin()
userOwner := c.Input().Get("owner")
userName := c.Input().Get("name")
var options *protocol.CredentialAssertion
var sessionData *webauthn.SessionData
if userName == "" {
options, sessionData, err = webauthnObj.BeginDiscoverableLogin()
} else {
var user *object.User
user, err = object.GetUserByFields(userOwner, userName)
if err != nil {
c.ResponseError(err.Error())
return
}
if user == nil {
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(userOwner, userName)))
return
}
if len(user.WebauthnCredentials) == 0 {
c.ResponseError(c.T("webauthn:Found no credentials for this user"))
return
}
options, sessionData, err = webauthnObj.BeginLogin(user)
}
if err != nil {
c.ResponseError(err.Error())
return
@@ -153,15 +188,27 @@ func (c *ApiController) WebAuthnSigninFinish() {
c.Ctx.Request.Body = io.NopCloser(bytes.NewBuffer(c.Ctx.Input.RequestBody))
var user *object.User
handler := func(rawID, userHandle []byte) (webauthn.User, error) {
user, err = object.GetUserByWebauthID(base64.StdEncoding.EncodeToString(rawID))
if sessionData.UserID != nil {
userId := string(sessionData.UserID)
user, err = object.GetUser(userId)
if err != nil {
return nil, err
c.ResponseError(err.Error())
return
}
return user, nil
_, err = webauthnObj.FinishLogin(user, sessionData, c.Ctx.Request)
} else {
handler := func(rawID, userHandle []byte) (webauthn.User, error) {
user, err = object.GetUserByWebauthID(base64.StdEncoding.EncodeToString(rawID))
if err != nil {
return nil, err
}
return user, nil
}
_, err = webauthnObj.FinishDiscoverableLogin(handler, sessionData, c.Ctx.Request)
}
_, err = webauthnObj.FinishDiscoverableLogin(handler, sessionData, c.Ctx.Request)
if err != nil {
c.ResponseError(err.Error())
return

View File

@@ -637,9 +637,6 @@ class LoginPage extends React.Component {
)
;
} else if (signinItem.name === "Username") {
if (this.state.loginMethod === "webAuthn") {
return null;
}
return (
<div key={resultItemKey}>
<div dangerouslySetInnerHTML={{__html: ("<style>" + signinItem.customCss?.replaceAll("<style>", "").replaceAll("</style>", "") + "</style>")}} />
@@ -649,7 +646,7 @@ class LoginPage extends React.Component {
label={signinItem.label ? signinItem.label : null}
rules={[
{
required: true,
required: this.state.loginMethod !== "webAuthn",
message: () => {
switch (this.state.loginMethod) {
case "verificationCodeEmail":
@@ -1093,7 +1090,8 @@ class LoginPage extends React.Component {
const oAuthParams = Util.getOAuthGetParameters();
this.populateOauthValues(values);
const application = this.getApplicationObj();
return fetch(`${Setting.ServerUrl}/api/webauthn/signin/begin?owner=${application.organization}`, {
const usernameParam = `&name=${encodeURIComponent(username)}`;
return fetch(`${Setting.ServerUrl}/api/webauthn/signin/begin?owner=${application.organization}${username ? usernameParam : ""}`, {
method: "GET",
credentials: "include",
})
@@ -1105,6 +1103,12 @@ class LoginPage extends React.Component {
}
credentialRequestOptions.publicKey.challenge = UserWebauthnBackend.webAuthnBufferDecode(credentialRequestOptions.publicKey.challenge);
if (username) {
credentialRequestOptions.publicKey.allowCredentials.forEach(function(listItem) {
listItem.id = UserWebauthnBackend.webAuthnBufferDecode(listItem.id);
});
}
return navigator.credentials.get({
publicKey: credentialRequestOptions.publicKey,
});

View File

@@ -31,6 +31,7 @@ export function registerWebauthnCredential() {
credentialCreationOptions.publicKey.excludeCredentials[i].id = webAuthnBufferDecode(credentialCreationOptions.publicKey.excludeCredentials[i].id);
}
}
return navigator.credentials.create({
publicKey: credentialCreationOptions.publicKey,
});

View File

@@ -42,8 +42,9 @@ class WebAuthnCredentialTable extends React.Component {
const columns = [
{
title: i18next.t("general:Name"),
dataIndex: "ID",
key: "ID",
dataIndex: "id",
key: "id",
ellipsis: true,
},
{
title: i18next.t("general:Action"),
@@ -60,7 +61,7 @@ class WebAuthnCredentialTable extends React.Component {
];
return (
<Table rowKey={"ID"} columns={columns} dataSource={this.props.table} size="middle" bordered pagination={false}
<Table rowKey={"id"} columns={columns} dataSource={this.props.table} size="middle" bordered pagination={false}
title={() => (
<div>
{i18next.t("user:WebAuthn credentials")}&nbsp;&nbsp;&nbsp;&nbsp;