This commit is contained in:
owenchen committed 2026-08-06 13:15:56 +08:00
1 parent 7eb017d8ee
commit 873f65dbbe
48 files changed
+1391 -1236

No files matched your search

@@ -22,6 +22,7 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="8.0.0" />
<PackageReference Include="Fido2NetLib" Version="1.0.0-alpha" />
</ItemGroup>
<ItemGroup>
@@ -0,0 +1,19 @@
using CloudBuilder.Core.Authorization;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CloudBuilder.Security.Data
{
[TypeScript]
public class BiometricLoginChallengeDto
{
/// <summary>AssertionOptions JSON to feed to navigator.credentials.get().</summary>
public string RequestOptionsJson { get; set; } = null!;
/// <summary>Opaque id identifying this challenge — we round-trip the requestOptionsJson instead, so this is informational and currently unused.</summary>
public string ChallengeId { get; set; } = string.Empty;
}
}
@@ -0,0 +1,15 @@
using CloudBuilder.Core.Authorization;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CloudBuilder.Security.Data
{
[TypeScript]
public class BiometricRegistrationResultDto
{
public string CredentialIdBase64Url { get; set; } = string.Empty;
}
}
@@ -0,0 +1,20 @@
using CloudBuilder.Core.Authorization;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CloudBuilder.Security.Data
{
[TypeScript]
public class FinishBiometricLoginData
{
public string Username { get; set; } = null!;
public string AssertionJson { get; set; } = null!;
public string RequestOptionsJson { get; set; } = null!;
/// <summary>Opaque id; informational only. We round-trip the requestOptionsJson.</summary>
public string ChallengeId { get; set; } = string.Empty;
}
}
@@ -0,0 +1,19 @@
using CloudBuilder.Core.Authorization;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CloudBuilder.Security.Data
{
[TypeScript]
public class FinishBiometricRegistrationData
{
/// <summary>JSON the browser returns from navigator.credentials.create().</summary>
public string AttestationJson { get; set; } = null!;
/// <summary>The CredentialCreateOptions JSON we returned from BeginBiometricRegistration. Round-tripped because Fido2NetLib verifies the challenge against it.</summary>
public string CreationOptionsJson { get; set; } = null!;
}
}
@@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CloudBuilder.Security.Data
{
/// <summary>
/// Plain-old-data wrapper for the credential fields we persist on
/// SecurityUser. The frontend never sees this type.
/// </summary>
public class StoredBiometricCredential
{
public byte[] CredentialId { get; set; } = Array.Empty<byte>();
public byte[] PublicKey { get; set; } = Array.Empty<byte>();
}
public class WebAuthnOptions
{
/// <summary>
/// Relying Party ID — the registrable domain suffix of the
/// origin's host. For dev: "localhost". For production:
/// "eleave.1010printing.com".
/// </summary>
public string RpId { get; set; } = "localhost";
/// <summary>
/// Human-readable relying-party name. Shown in the OS biometric
/// prompt ("Use HR Plus to sign in to localhost").
/// </summary>
public string RpName { get; set; } = "HR Plus";
/// <summary>
/// The full origin (scheme + host + port) the user is loading
/// the page from. Fido2NetLib verifies the attestation/assertion
/// origin matches this exactly. Must be HTTPS in production
/// (localhost is exempt).
/// </summary>
public string Origin { get; set; } = "https://localhost:8000";
/// <summary>
/// WebAuthn ceremony timeout in milliseconds. Default 60s.
/// </summary>
public int TimeoutMs { get; set; } = 60000;
}
}
@@ -26,42 +26,22 @@ namespace CloudBuilder.Security.Entity
public string UserName { get; set; }
[Column("user_local_name")]
public string UserLocalName { get; set; }
[Column("user_eng_name")]
public string? UserEngName { get; set; }
[Column("company_name")]
public string? CompanyName { get; set; }
[Column("industry")]
public string? Industry { get; set; }
[Column("user_password")]
public string? UserPassword { get; set; }
[Column("user_mobile")]
public string? UserMobile { get; set; }
[Column("user_address")]
public string? UserAddress { get; set; }
[Column("postcode")]
public string? Postcode { get; set; }
[Column("community")]
public int Community { get; set; }
[Column("user_grade")]
public int UserGrade { get; set; }
[Column("user_type")]
public string? UserType { get; set; }
[Column("user_wechat_account")]
public string? UserWechatAccount { get; set; }
[Column("user_alipay_account")]
public string? UserAlipayAccount { get; set; }
[Column("user_alipay_account_img_path")]
public string? UserAlipayAccountImgPath { get; set; }
[Column("user_wechat_account_img_path")]
public string? UserWechatAccountImgPath { get; set; }
[Column("user_icon_img_path")]
public string? UserIconImgPath { get; set; }
[Column("user_accounts_expire_datetime")]
public Nullable<DateTime> UserAccountsExpireDatetime { get; set; }
[Column("user_is_valid_indc")]
public string? UserIsValidIndc { get; set; }
[Column("is_forece_change_password_indc")]
public string? IsForeceChangePasswordIndc { get; set; }
[Column("biometric_enrolled_at")]
public Nullable<DateTime> BiometricEnrolledAt { get; set; }
[Column("biometric_last_used_at")]
public Nullable<DateTime> BiometricLastUsedAt { get; set; }
[Column("last_login_datetime")]
public Nullable<DateTime> LastLoginDatetime { get; set; }
[Column("error_login_count")]
public int ErrorLoginCount { get; set; }
[Column("biometric_credential_id")]
public string? BiometricCredentialId { get; set; }
[Column("biometric_public_key")]
public string? BiometricPublicKey { get; set; }
[Column("biometric_counter")]
public int BiometricCounter { get; set; }
[Column("biometric_device_name")]
public string? BiometricDeviceName { get; set; }
[Column("is_accounts_locked_indc")]
public string? IsAccountsLockedIndc { get; set; }
[Column("accounts_locked_datetime")]
@@ -74,10 +54,42 @@ namespace CloudBuilder.Security.Entity
public Nullable<DateTime> UserTypeEndDatetime { get; set; }
[Column("room_name")]
public string? RoomName { get; set; }
[Column("last_login_datetime")]
public Nullable<DateTime> LastLoginDatetime { get; set; }
[Column("error_login_count")]
public int ErrorLoginCount { get; set; }
[Column("user_alipay_account_img_path")]
public string? UserAlipayAccountImgPath { get; set; }
[Column("user_wechat_account_img_path")]
public string? UserWechatAccountImgPath { get; set; }
[Column("user_icon_img_path")]
public string? UserIconImgPath { get; set; }
[Column("user_accounts_expire_datetime")]
public Nullable<DateTime> UserAccountsExpireDatetime { get; set; }
[Column("user_is_valid_indc")]
public string? UserIsValidIndc { get; set; }
[Column("is_forece_change_password_indc")]
public string? IsForeceChangePasswordIndc { get; set; }
[Column("postcode")]
public string? Postcode { get; set; }
[Column("community")]
public int Community { get; set; }
[Column("user_grade")]
public int UserGrade { get; set; }
[Column("user_type")]
public string? UserType { get; set; }
[Column("user_wechat_account")]
public string? UserWechatAccount { get; set; }
[Column("user_alipay_account")]
public string? UserAlipayAccount { get; set; }
[Column("user_eng_name")]
public string? UserEngName { get; set; }
[Column("company_name")]
public string? CompanyName { get; set; }
[Column("industry")]
public string? Industry { get; set; }
[Column("user_password")]
public string? UserPassword { get; set; }
[Column("user_mobile")]
public string? UserMobile { get; set; }
[Column("user_address")]
public string? UserAddress { get; set; }
public const string CREATED_DATETIME = "CreatedDatetime";
public const string CREATED_BY = "CreatedBy";
@@ -85,35 +97,41 @@ namespace CloudBuilder.Security.Entity
public const string UPDATED_BY = "UpdatedBy";
public const string USER_NAME = "UserName";
public const string USER_LOCAL_NAME = "UserLocalName";
public const string USER_ENG_NAME = "UserEngName";
public const string COMPANY_NAME = "CompanyName";
public const string INDUSTRY = "Industry";
public const string USER_PASSWORD = "UserPassword";
public const string USER_MOBILE = "UserMobile";
public const string USER_ADDRESS = "UserAddress";
public const string POSTCODE = "Postcode";
public const string COMMUNITY = "Community";
public const string USER_GRADE = "UserGrade";
public const string USER_TYPE = "UserType";
public const string USER_WECHAT_ACCOUNT = "UserWechatAccount";
public const string USER_ALIPAY_ACCOUNT = "UserAlipayAccount";
public const string USER_ALIPAY_ACCOUNT_IMG_PATH = "UserAlipayAccountImgPath";
public const string USER_WECHAT_ACCOUNT_IMG_PATH = "UserWechatAccountImgPath";
public const string USER_ICON_IMG_PATH = "UserIconImgPath";
public const string USER_ACCOUNTS_EXPIRE_DATETIME = "UserAccountsExpireDatetime";
public const string USER_IS_VALID_INDC = "UserIsValidIndc";
public const string IS_FORECE_CHANGE_PASSWORD_INDC = "IsForeceChangePasswordIndc";
public const string BIOMETRIC_ENROLLED_AT = "BiometricEnrolledAt";
public const string BIOMETRIC_LAST_USED_AT = "BiometricLastUsedAt";
public const string LAST_LOGIN_DATETIME = "LastLoginDatetime";
public const string ERROR_LOGIN_COUNT = "ErrorLoginCount";
public const string BIOMETRIC_CREDENTIAL_ID = "BiometricCredentialId";
public const string BIOMETRIC_PUBLIC_KEY = "BiometricPublicKey";
public const string BIOMETRIC_COUNTER = "BiometricCounter";
public const string BIOMETRIC_DEVICE_NAME = "BiometricDeviceName";
public const string IS_ACCOUNTS_LOCKED_INDC = "IsAccountsLockedIndc";
public const string ACCOUNTS_LOCKED_DATETIME = "AccountsLockedDatetime";
public const string USER_REMARK = "UserRemark";
public const string WELCOME_PAGE = "WelcomePage";
public const string USER_TYPE_END_DATETIME = "UserTypeEndDatetime";
public const string ROOM_NAME = "RoomName";
public const string LAST_LOGIN_DATETIME = "LastLoginDatetime";
public const string ERROR_LOGIN_COUNT = "ErrorLoginCount";
public const string USER_ALIPAY_ACCOUNT_IMG_PATH = "UserAlipayAccountImgPath";
public const string USER_WECHAT_ACCOUNT_IMG_PATH = "UserWechatAccountImgPath";
public const string USER_ICON_IMG_PATH = "UserIconImgPath";
public const string USER_ACCOUNTS_EXPIRE_DATETIME = "UserAccountsExpireDatetime";
public const string USER_IS_VALID_INDC = "UserIsValidIndc";
public const string IS_FORECE_CHANGE_PASSWORD_INDC = "IsForeceChangePasswordIndc";
public const string POSTCODE = "Postcode";
public const string COMMUNITY = "Community";
public const string USER_GRADE = "UserGrade";
public const string USER_TYPE = "UserType";
public const string USER_WECHAT_ACCOUNT = "UserWechatAccount";
public const string USER_ALIPAY_ACCOUNT = "UserAlipayAccount";
public const string USER_ENG_NAME = "UserEngName";
public const string COMPANY_NAME = "CompanyName";
public const string INDUSTRY = "Industry";
public const string USER_PASSWORD = "UserPassword";
public const string USER_MOBILE = "UserMobile";
public const string USER_ADDRESS = "UserAddress";
public const string DB_NAME_SECURITY_USER = "security_user";
public const string DB_NAME_FIELDS="created_datetime,created_by,updated_datetime,updated_by,user_name,user_local_name,user_eng_name,company_name,industry,user_password,user_mobile,user_address,postcode,community,user_grade,user_type,user_wechat_account,user_alipay_account,user_alipay_account_img_path,user_wechat_account_img_path,user_icon_img_path,user_accounts_expire_datetime,user_is_valid_indc,is_forece_change_password_indc,is_accounts_locked_indc,accounts_locked_datetime,user_remark,welcome_page,user_type_end_datetime,room_name,last_login_datetime,error_login_count";
public const string DB_NAME_FIELDS="created_datetime,created_by,updated_datetime,updated_by,user_name,user_local_name,biometric_enrolled_at,biometric_last_used_at,last_login_datetime,error_login_count,biometric_credential_id,biometric_public_key,biometric_counter,biometric_device_name,is_accounts_locked_indc,accounts_locked_datetime,user_remark,welcome_page,user_type_end_datetime,room_name,user_alipay_account_img_path,user_wechat_account_img_path,user_icon_img_path,user_accounts_expire_datetime,user_is_valid_indc,is_forece_change_password_indc,postcode,community,user_grade,user_type,user_wechat_account,user_alipay_account,user_eng_name,company_name,industry,user_password,user_mobile,user_address";
public const string DB_CREATED_DATETIME = "created_datetime";
public const string DB_CREATED_BY = "created_by";
@@ -121,32 +139,38 @@ namespace CloudBuilder.Security.Entity
public const string DB_UPDATED_BY = "updated_by";
public const string DB_USER_NAME = "user_name";
public const string DB_USER_LOCAL_NAME = "user_local_name";
public const string DB_USER_ENG_NAME = "user_eng_name";
public const string DB_COMPANY_NAME = "company_name";
public const string DB_INDUSTRY = "industry";
public const string DB_USER_PASSWORD = "user_password";
public const string DB_USER_MOBILE = "user_mobile";
public const string DB_USER_ADDRESS = "user_address";
public const string DB_POSTCODE = "postcode";
public const string DB_COMMUNITY = "community";
public const string DB_USER_GRADE = "user_grade";
public const string DB_USER_TYPE = "user_type";
public const string DB_USER_WECHAT_ACCOUNT = "user_wechat_account";
public const string DB_USER_ALIPAY_ACCOUNT = "user_alipay_account";
public const string DB_USER_ALIPAY_ACCOUNT_IMG_PATH = "user_alipay_account_img_path";
public const string DB_USER_WECHAT_ACCOUNT_IMG_PATH = "user_wechat_account_img_path";
public const string DB_USER_ICON_IMG_PATH = "user_icon_img_path";
public const string DB_USER_ACCOUNTS_EXPIRE_DATETIME = "user_accounts_expire_datetime";
public const string DB_USER_IS_VALID_INDC = "user_is_valid_indc";
public const string DB_IS_FORECE_CHANGE_PASSWORD_INDC = "is_forece_change_password_indc";
public const string DB_BIOMETRIC_ENROLLED_AT = "biometric_enrolled_at";
public const string DB_BIOMETRIC_LAST_USED_AT = "biometric_last_used_at";
public const string DB_LAST_LOGIN_DATETIME = "last_login_datetime";
public const string DB_ERROR_LOGIN_COUNT = "error_login_count";
public const string DB_BIOMETRIC_CREDENTIAL_ID = "biometric_credential_id";
public const string DB_BIOMETRIC_PUBLIC_KEY = "biometric_public_key";
public const string DB_BIOMETRIC_COUNTER = "biometric_counter";
public const string DB_BIOMETRIC_DEVICE_NAME = "biometric_device_name";
public const string DB_IS_ACCOUNTS_LOCKED_INDC = "is_accounts_locked_indc";
public const string DB_ACCOUNTS_LOCKED_DATETIME = "accounts_locked_datetime";
public const string DB_USER_REMARK = "user_remark";
public const string DB_WELCOME_PAGE = "welcome_page";
public const string DB_USER_TYPE_END_DATETIME = "user_type_end_datetime";
public const string DB_ROOM_NAME = "room_name";
public const string DB_LAST_LOGIN_DATETIME = "last_login_datetime";
public const string DB_ERROR_LOGIN_COUNT = "error_login_count";
public const string DB_USER_ALIPAY_ACCOUNT_IMG_PATH = "user_alipay_account_img_path";
public const string DB_USER_WECHAT_ACCOUNT_IMG_PATH = "user_wechat_account_img_path";
public const string DB_USER_ICON_IMG_PATH = "user_icon_img_path";
public const string DB_USER_ACCOUNTS_EXPIRE_DATETIME = "user_accounts_expire_datetime";
public const string DB_USER_IS_VALID_INDC = "user_is_valid_indc";
public const string DB_IS_FORECE_CHANGE_PASSWORD_INDC = "is_forece_change_password_indc";
public const string DB_POSTCODE = "postcode";
public const string DB_COMMUNITY = "community";
public const string DB_USER_GRADE = "user_grade";
public const string DB_USER_TYPE = "user_type";
public const string DB_USER_WECHAT_ACCOUNT = "user_wechat_account";
public const string DB_USER_ALIPAY_ACCOUNT = "user_alipay_account";
public const string DB_USER_ENG_NAME = "user_eng_name";
public const string DB_COMPANY_NAME = "company_name";
public const string DB_INDUSTRY = "industry";
public const string DB_USER_PASSWORD = "user_password";
public const string DB_USER_MOBILE = "user_mobile";
public const string DB_USER_ADDRESS = "user_address";
public static SecurityUserEntity GetInstance()
@@ -172,32 +196,38 @@ namespace CloudBuilder.Security.Entity
this.UpdatedBy = source.UpdatedBy;
this.UserName = source.UserName;
this.UserLocalName = source.UserLocalName;
this.UserEngName = source.UserEngName;
this.CompanyName = source.CompanyName;
this.Industry = source.Industry;
this.UserPassword = source.UserPassword;
this.UserMobile = source.UserMobile;
this.UserAddress = source.UserAddress;
this.Postcode = source.Postcode;
this.Community = source.Community;
this.UserGrade = source.UserGrade;
this.UserType = source.UserType;
this.UserWechatAccount = source.UserWechatAccount;
this.UserAlipayAccount = source.UserAlipayAccount;
this.UserAlipayAccountImgPath = source.UserAlipayAccountImgPath;
this.UserWechatAccountImgPath = source.UserWechatAccountImgPath;
this.UserIconImgPath = source.UserIconImgPath;
this.UserAccountsExpireDatetime = source.UserAccountsExpireDatetime;
this.UserIsValidIndc = source.UserIsValidIndc;
this.IsForeceChangePasswordIndc = source.IsForeceChangePasswordIndc;
this.BiometricEnrolledAt = source.BiometricEnrolledAt;
this.BiometricLastUsedAt = source.BiometricLastUsedAt;
this.LastLoginDatetime = source.LastLoginDatetime;
this.ErrorLoginCount = source.ErrorLoginCount;
this.BiometricCredentialId = source.BiometricCredentialId;
this.BiometricPublicKey = source.BiometricPublicKey;
this.BiometricCounter = source.BiometricCounter;
this.BiometricDeviceName = source.BiometricDeviceName;
this.IsAccountsLockedIndc = source.IsAccountsLockedIndc;
this.AccountsLockedDatetime = source.AccountsLockedDatetime;
this.UserRemark = source.UserRemark;
this.WelcomePage = source.WelcomePage;
this.UserTypeEndDatetime = source.UserTypeEndDatetime;
this.RoomName = source.RoomName;
this.LastLoginDatetime = source.LastLoginDatetime;
this.ErrorLoginCount = source.ErrorLoginCount;
this.UserAlipayAccountImgPath = source.UserAlipayAccountImgPath;
this.UserWechatAccountImgPath = source.UserWechatAccountImgPath;
this.UserIconImgPath = source.UserIconImgPath;
this.UserAccountsExpireDatetime = source.UserAccountsExpireDatetime;
this.UserIsValidIndc = source.UserIsValidIndc;
this.IsForeceChangePasswordIndc = source.IsForeceChangePasswordIndc;
this.Postcode = source.Postcode;
this.Community = source.Community;
this.UserGrade = source.UserGrade;
this.UserType = source.UserType;
this.UserWechatAccount = source.UserWechatAccount;
this.UserAlipayAccount = source.UserAlipayAccount;
this.UserEngName = source.UserEngName;
this.CompanyName = source.CompanyName;
this.Industry = source.Industry;
this.UserPassword = source.UserPassword;
this.UserMobile = source.UserMobile;
this.UserAddress = source.UserAddress;
if (includeSystemFields)
{
@@ -24,5 +24,16 @@ namespace CloudBuilder.Security.Service
void Logout();
string GetRsaPublicKey();
BiometricLoginChallengeDto BeginBiometricLogin(string username);
LoginProfile FinishBiometricLogin(FinishBiometricLoginData dto);
bool ConfirmPassword(string password);
string BeginBiometricRegistration(string deviceName);
BiometricRegistrationResultDto FinishBiometricRegistration(FinishBiometricRegistrationData dto);
void RemoveBiometric();
}
}
@@ -0,0 +1,32 @@
using Fido2NetLib;
using Fido2NetLib.Objects;
using CloudBuilder.Core.Service;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CloudBuilder.Security.Data;
namespace CloudBuilder.Security.Service
{
public interface IWebAuthnService : IService
{
CredentialCreateOptions BuildCreationOptions(string username, byte[] userId, IReadOnlyList<byte[]> existingCredentialIds);
Task<StoredBiometricCredential> FinishRegistrationAsync(
AuthenticatorAttestationRawResponse attestationResponse,
CredentialCreateOptions originalOptions,
Func<byte[], Task<bool>> isCredentialIdUnique);
AssertionOptions BuildRequestOptions(byte[] storedCredentialId);
string BuildRequestOptionsJson(byte[] storedCredentialId);
Task<uint> FinishAssertionAsync(
AuthenticatorAssertionRawResponse assertionResponse,
AssertionOptions originalOptions,
byte[] storedPublicKey,
uint storedCounter);
}
}