|
|
@@ -0,0 +1,774 @@
|
|
|
+/*
|
|
|
+ * Copyright [2024] [MaxKey of copyright http://www.maxkey.top]
|
|
|
+ *
|
|
|
+ * Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
+ * you may not use this file except in compliance with the License.
|
|
|
+ * You may obtain a copy of the License at
|
|
|
+ *
|
|
|
+ * http://www.apache.org/licenses/LICENSE-2.0
|
|
|
+ *
|
|
|
+ * Unless required by applicable law or agreed to in writing, software
|
|
|
+ * distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
+ * See the License for the specific language governing permissions and
|
|
|
+ * limitations under the License.
|
|
|
+ */
|
|
|
+
|
|
|
+package org.dromara.maxkey.passkey.service.impl;
|
|
|
+
|
|
|
+import org.dromara.maxkey.passkey.service.PasskeyService;
|
|
|
+import org.dromara.maxkey.entity.passkey.UserPasskey;
|
|
|
+import org.dromara.maxkey.entity.passkey.PasskeyChallenge;
|
|
|
+import org.dromara.maxkey.passkey.config.PasskeyProperties;
|
|
|
+import org.dromara.maxkey.persistence.service.UserPasskeyService;
|
|
|
+import org.dromara.maxkey.persistence.service.PasskeyChallengeService;
|
|
|
+import org.dromara.maxkey.util.IdGenerator;
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+
|
|
|
+// WebAuthn4J imports
|
|
|
+import com.webauthn4j.WebAuthnManager;
|
|
|
+import com.webauthn4j.converter.util.ObjectConverter;
|
|
|
+import com.webauthn4j.data.*;
|
|
|
+import com.webauthn4j.data.client.*;
|
|
|
+import com.webauthn4j.data.attestation.*;
|
|
|
+import com.webauthn4j.server.ServerProperty;
|
|
|
+import com.webauthn4j.data.client.Origin;
|
|
|
+import com.webauthn4j.data.client.challenge.Challenge;
|
|
|
+import com.webauthn4j.data.client.challenge.DefaultChallenge;
|
|
|
+import com.webauthn4j.converter.exception.DataConversionException;
|
|
|
+import com.webauthn4j.data.RegistrationData;
|
|
|
+import com.webauthn4j.data.RegistrationParameters;
|
|
|
+import com.webauthn4j.data.AuthenticationData;
|
|
|
+import com.webauthn4j.data.AuthenticationParameters;
|
|
|
+import com.webauthn4j.verifier.exception.VerificationException;
|
|
|
+import com.webauthn4j.credential.CredentialRecord;
|
|
|
+import com.webauthn4j.credential.CredentialRecordImpl;
|
|
|
+import com.webauthn4j.data.attestation.authenticator.AttestedCredentialData;
|
|
|
+import com.webauthn4j.data.attestation.authenticator.COSEKey;
|
|
|
+import com.webauthn4j.data.attestation.authenticator.AAGUID;
|
|
|
+
|
|
|
+// Passkey utility imports
|
|
|
+import org.dromara.maxkey.passkey.util.PasskeyUtils;
|
|
|
+
|
|
|
+import java.util.*;
|
|
|
+import java.security.SecureRandom;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.concurrent.ConcurrentHashMap;
|
|
|
+import org.apache.commons.codec.binary.Base64;
|
|
|
+import java.util.Objects;
|
|
|
+
|
|
|
+/**
|
|
|
+ * Passkey服务实现类 - 重构版本
|
|
|
+ * 通过方法拆分和工具类提取,提高代码可维护性和可读性
|
|
|
+ */
|
|
|
+@Service
|
|
|
+public class PasskeyServiceImpl implements PasskeyService {
|
|
|
+ private static final Logger _logger = LoggerFactory.getLogger(PasskeyServiceImpl.class);
|
|
|
+
|
|
|
+ // 常量定义
|
|
|
+ private static final String CHALLENGE_TYPE_REGISTRATION = "REGISTRATION";
|
|
|
+ private static final String CHALLENGE_TYPE_AUTHENTICATION = "AUTHENTICATION";
|
|
|
+ private static final String CREDENTIAL_TYPE_PUBLIC_KEY = "public-key";
|
|
|
+ private static final String DEFAULT_INST_ID = "1";
|
|
|
+ private static final String DEFAULT_DEVICE_NAME = "Passkey 设备";
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private WebAuthnManager webAuthnManager;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private ObjectConverter objectConverter;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private PasskeyProperties passkeyProperties;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private List<PublicKeyCredentialParameters> publicKeyCredentialParameters;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private UserPasskeyService userPasskeyService;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private PasskeyChallengeService passkeyChallengeService;
|
|
|
+
|
|
|
+ private final SecureRandom secureRandom = new SecureRandom();
|
|
|
+ private final IdGenerator idGenerator = new IdGenerator();
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Map<String, Object> generateRegistrationOptions(String userId, String username, String displayName) {
|
|
|
+ _logger.debug("Generating registration options for user: {}", userId);
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 生成并保存挑战
|
|
|
+ String challengeId = generateAndSaveChallenge(userId, CHALLENGE_TYPE_REGISTRATION);
|
|
|
+ String challengeBase64 = getChallenge(challengeId).getChallenge();
|
|
|
+
|
|
|
+ // 构建注册选项
|
|
|
+ Map<String, Object> options = buildRegistrationOptions(userId, username, displayName, challengeId, challengeBase64);
|
|
|
+
|
|
|
+ _logger.debug("Registration options generated successfully for user: {}", userId);
|
|
|
+ return options;
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ _logger.error("Error generating registration options for user: {}", userId, e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成并保存挑战
|
|
|
+ */
|
|
|
+ private String generateAndSaveChallenge(String userId, String challengeType) {
|
|
|
+ PasskeyChallenge passkeyChallenge = PasskeyUtils.generateChallenge(
|
|
|
+ userId, challengeType, passkeyProperties.getChallenge().getLength());
|
|
|
+ passkeyChallenge.setInstId(DEFAULT_INST_ID);
|
|
|
+ passkeyChallengeService.saveChallenge(passkeyChallenge);
|
|
|
+
|
|
|
+ return passkeyChallenge.getId();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建注册选项
|
|
|
+ */
|
|
|
+ private Map<String, Object> buildRegistrationOptions(String userId, String username, String displayName,
|
|
|
+ String challengeId, String challengeBase64) {
|
|
|
+ Map<String, Object> options = new HashMap<>();
|
|
|
+ options.put("challenge", challengeBase64);
|
|
|
+ options.put("challengeId", challengeId);
|
|
|
+ options.put("timeout", passkeyProperties.getChallenge().getTimeoutMs());
|
|
|
+ options.put("attestation", passkeyProperties.getAuthenticator().getAttestation());
|
|
|
+
|
|
|
+ // RP信息
|
|
|
+ options.put("rp", buildRelyingPartyInfo());
|
|
|
+
|
|
|
+ // 用户信息
|
|
|
+ options.put("user", buildUserInfo(userId, username, displayName));
|
|
|
+
|
|
|
+ // 公钥凭据参数
|
|
|
+ options.put("pubKeyCredParams", buildPublicKeyCredentialParams());
|
|
|
+
|
|
|
+ // 认证器选择标准
|
|
|
+ options.put("authenticatorSelection", buildAuthenticatorSelection());
|
|
|
+
|
|
|
+ // 排除凭据
|
|
|
+ List<Map<String, Object>> excludeCredentials = buildExcludeCredentials(userId);
|
|
|
+ if (!excludeCredentials.isEmpty()) {
|
|
|
+ options.put("excludeCredentials", excludeCredentials);
|
|
|
+ }
|
|
|
+
|
|
|
+ return options;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建RP信息
|
|
|
+ */
|
|
|
+ private Map<String, Object> buildRelyingPartyInfo() {
|
|
|
+ return PasskeyUtils.buildRelyingPartyInfo(passkeyProperties.getRelyingParty());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建用户信息
|
|
|
+ */
|
|
|
+ private Map<String, Object> buildUserInfo(String userId, String username, String displayName) {
|
|
|
+ return PasskeyUtils.buildUserInfo(userId, username, displayName);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建公钥凭据参数
|
|
|
+ */
|
|
|
+ private List<Map<String, Object>> buildPublicKeyCredentialParams() {
|
|
|
+ return PasskeyUtils.buildPublicKeyCredentialParams(publicKeyCredentialParameters);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建认证器选择标准
|
|
|
+ */
|
|
|
+ private Map<String, Object> buildAuthenticatorSelection() {
|
|
|
+ return PasskeyUtils.buildAuthenticatorSelection(passkeyProperties.getAuthenticator());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建排除凭据列表
|
|
|
+ */
|
|
|
+ private List<Map<String, Object>> buildExcludeCredentials(String userId) {
|
|
|
+ List<UserPasskey> existingPasskeys = userPasskeyService.findByUserId(userId);
|
|
|
+ return PasskeyUtils.buildCredentialList(existingPasskeys);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public UserPasskey verifyRegistrationResponse(String userId, Map<String, Object> registrationResponse) {
|
|
|
+ _logger.debug("Verifying registration response for user: {}", userId);
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 验证挑战
|
|
|
+ PasskeyChallenge challenge = validateChallenge(registrationResponse, CHALLENGE_TYPE_REGISTRATION);
|
|
|
+ if (challenge == null) {
|
|
|
+ _logger.warn("Invalid or expired registration challenge for user: {}", userId);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解析注册响应数据
|
|
|
+ RegistrationResponseData responseData = parseRegistrationResponse(registrationResponse);
|
|
|
+ if (responseData == null) {
|
|
|
+ _logger.warn("Failed to parse registration response for user: {}", userId);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建服务器属性
|
|
|
+ ServerProperty serverProperty = createServerProperty(responseData.clientDataJSON, challenge.getChallenge());
|
|
|
+ if (serverProperty == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 执行WebAuthn验证
|
|
|
+ RegistrationData registrationData = performRegistrationVerification(responseData, serverProperty);
|
|
|
+ if (registrationData == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建并保存Passkey
|
|
|
+ UserPasskey userPasskey = createUserPasskey(userId, responseData.credentialIdBase64, registrationData);
|
|
|
+ boolean saved = savePasskey(userPasskey);
|
|
|
+
|
|
|
+ // 标记挑战为已使用
|
|
|
+ challenge.setStatus(1);
|
|
|
+ passkeyChallengeService.saveChallenge(challenge);
|
|
|
+
|
|
|
+ _logger.debug("Registration verification completed for user: {}, result: {}", userId, saved);
|
|
|
+ return saved ? userPasskey : null;
|
|
|
+
|
|
|
+ } catch (VerificationException e) {
|
|
|
+ _logger.error("WebAuthn validation failed for user: {}", userId, e);
|
|
|
+ return null;
|
|
|
+ } catch (Exception e) {
|
|
|
+ _logger.error("Error verifying registration response for user: {}", userId, e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 注册响应数据结构
|
|
|
+ */
|
|
|
+ private static class RegistrationResponseData {
|
|
|
+ String credentialIdBase64;
|
|
|
+ String attestationObjectBase64;
|
|
|
+ String clientDataJSONBase64;
|
|
|
+ byte[] clientDataJSON;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 验证挑战
|
|
|
+ */
|
|
|
+ private PasskeyChallenge validateChallenge(Map<String, Object> response, String expectedType) {
|
|
|
+ String challengeId = (String) response.get("challengeId");
|
|
|
+ _logger.debug("Validating challenge with ID: {} and expected type: {}", challengeId, expectedType);
|
|
|
+
|
|
|
+ PasskeyChallenge challenge = passkeyChallengeService.findByChallengeId(challengeId);
|
|
|
+
|
|
|
+ if (challenge == null) {
|
|
|
+ _logger.warn("Challenge not found for ID: {}", challengeId);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ _logger.debug("Challenge found: {}", challenge.toString());
|
|
|
+ _logger.debug("Challenge expired: {}, Challenge type: {}, Expected type: {}, Status: {}",
|
|
|
+ challenge.isExpired(), challenge.getChallengeType(), expectedType, challenge.getStatus());
|
|
|
+
|
|
|
+ if (challenge.isExpired()) {
|
|
|
+ _logger.warn("Challenge expired for ID: {}", challengeId);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!expectedType.equals(challenge.getChallengeType())) {
|
|
|
+ _logger.warn("Challenge type mismatch for ID: {}. Expected: {}, Actual: {}",
|
|
|
+ challengeId, expectedType, challenge.getChallengeType());
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (challenge.getStatus() != null && challenge.getStatus() != 0) {
|
|
|
+ _logger.warn("Challenge already used or expired for ID: {}. Status: {}", challengeId, challenge.getStatus());
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ _logger.debug("Challenge validation successful for ID: {}", challengeId);
|
|
|
+ return challenge;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析注册响应
|
|
|
+ */
|
|
|
+ private RegistrationResponseData parseRegistrationResponse(Map<String, Object> registrationResponse) {
|
|
|
+ String credentialIdBase64 = (String) registrationResponse.get("credentialId");
|
|
|
+ String attestationObjectBase64 = (String) registrationResponse.get("attestationObject");
|
|
|
+ String clientDataJSONBase64 = (String) registrationResponse.get("clientDataJSON");
|
|
|
+
|
|
|
+ if (credentialIdBase64 == null || attestationObjectBase64 == null || clientDataJSONBase64 == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ RegistrationResponseData data = new RegistrationResponseData();
|
|
|
+ data.credentialIdBase64 = credentialIdBase64;
|
|
|
+ data.attestationObjectBase64 = attestationObjectBase64;
|
|
|
+ data.clientDataJSONBase64 = clientDataJSONBase64;
|
|
|
+ data.clientDataJSON = Base64.decodeBase64(clientDataJSONBase64);
|
|
|
+
|
|
|
+ return data;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建服务器属性
|
|
|
+ */
|
|
|
+ private ServerProperty createServerProperty(byte[] clientDataJSON, String challengeBase64) {
|
|
|
+ return PasskeyUtils.createServerProperty(
|
|
|
+ clientDataJSON, challengeBase64, passkeyProperties.getRelyingParty(), objectConverter);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 执行注册验证
|
|
|
+ */
|
|
|
+ private RegistrationData performRegistrationVerification(RegistrationResponseData responseData, ServerProperty serverProperty) {
|
|
|
+ try {
|
|
|
+ RegistrationParameters registrationParameters = new RegistrationParameters(
|
|
|
+ serverProperty,
|
|
|
+ publicKeyCredentialParameters,
|
|
|
+ false, // userVerificationRequired
|
|
|
+ true // userPresenceRequired
|
|
|
+ );
|
|
|
+
|
|
|
+ String registrationResponseJSON = objectConverter.getJsonConverter().writeValueAsString(
|
|
|
+ Map.of(
|
|
|
+ "id", responseData.credentialIdBase64,
|
|
|
+ "rawId", responseData.credentialIdBase64,
|
|
|
+ "response", Map.of(
|
|
|
+ "attestationObject", responseData.attestationObjectBase64,
|
|
|
+ "clientDataJSON", responseData.clientDataJSONBase64
|
|
|
+ ),
|
|
|
+ "type", CREDENTIAL_TYPE_PUBLIC_KEY
|
|
|
+ )
|
|
|
+ );
|
|
|
+
|
|
|
+ RegistrationData registrationData = webAuthnManager.parseRegistrationResponseJSON(registrationResponseJSON);
|
|
|
+ webAuthnManager.verify(registrationData, registrationParameters);
|
|
|
+
|
|
|
+ return registrationData;
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ _logger.error("Registration verification failed: {}", e.getMessage(), e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建UserPasskey对象
|
|
|
+ */
|
|
|
+ private UserPasskey createUserPasskey(String userId, String credentialIdBase64, RegistrationData registrationData) {
|
|
|
+ UserPasskey userPasskey = new UserPasskey();
|
|
|
+ userPasskey.setId(idGenerator.generate());
|
|
|
+ userPasskey.setUserId(userId);
|
|
|
+ userPasskey.setCredentialId(credentialIdBase64);
|
|
|
+
|
|
|
+ // 保存公钥信息
|
|
|
+ AttestedCredentialData attestedCredentialData = registrationData.getAttestationObject()
|
|
|
+ .getAuthenticatorData().getAttestedCredentialData();
|
|
|
+ if (attestedCredentialData != null) {
|
|
|
+ try {
|
|
|
+ userPasskey.setPublicKey(Base64.encodeBase64String(
|
|
|
+ objectConverter.getCborConverter().writeValueAsBytes(attestedCredentialData.getCOSEKey())
|
|
|
+ ));
|
|
|
+ } catch (Exception e) {
|
|
|
+ _logger.error("Failed to encode public key: {}", e.getMessage(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ userPasskey.setDisplayName(DEFAULT_DEVICE_NAME);
|
|
|
+ userPasskey.setDeviceType(passkeyProperties.getAuthenticator().getAttachment());
|
|
|
+ userPasskey.setInstId(DEFAULT_INST_ID);
|
|
|
+ userPasskey.setCreatedDate(new Date());
|
|
|
+ userPasskey.setLastUsedDate(new Date());
|
|
|
+ userPasskey.setSignatureCount(registrationData.getAttestationObject()
|
|
|
+ .getAuthenticatorData().getSignCount());
|
|
|
+
|
|
|
+ return userPasskey;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Map<String, Object> generateAuthenticationOptions(String userId) {
|
|
|
+ _logger.debug("Generating authentication options for usernameless authentication");
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 生成挑战
|
|
|
+ byte[] challenge = new byte[passkeyProperties.getChallenge().getLength()];
|
|
|
+ secureRandom.nextBytes(challenge);
|
|
|
+ String challengeBase64 = Base64.encodeBase64URLSafeString(challenge);
|
|
|
+
|
|
|
+ // 保存挑战信息 - 仅支持无用户名登录
|
|
|
+ String challengeId = new IdGenerator().generate();
|
|
|
+ PasskeyChallenge passkeyChallenge = new PasskeyChallenge(challengeId, challengeBase64, "AUTHENTICATION");
|
|
|
+ passkeyChallenge.setUserId(null); // 无用户名登录,userId 设为 null
|
|
|
+ passkeyChallenge.setInstId("1");
|
|
|
+ passkeyChallengeService.saveChallenge(passkeyChallenge);
|
|
|
+
|
|
|
+ // 构建认证选项
|
|
|
+ Map<String, Object> options = new HashMap<>();
|
|
|
+ options.put("challenge", challengeBase64);
|
|
|
+ options.put("challengeId", challengeId);
|
|
|
+ options.put("timeout", passkeyProperties.getChallenge().getTimeoutMs());
|
|
|
+ options.put("rpId", passkeyProperties.getRelyingParty().getId());
|
|
|
+ options.put("userVerification", passkeyProperties.getAuthenticator().getUserVerification());
|
|
|
+
|
|
|
+ // 无用户名登录:不设置 allowCredentials,让认证器自动选择
|
|
|
+ _logger.debug("Generated options for usernameless authentication");
|
|
|
+
|
|
|
+ return options;
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ _logger.error("Error generating authentication options for usernameless authentication", e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Map<String, Object> verifyAuthenticationResponse(Map<String, Object> authenticationResponse) {
|
|
|
+ _logger.debug("Verifying authentication response");
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 验证挑战
|
|
|
+ PasskeyChallenge challenge = validateChallenge(authenticationResponse, CHALLENGE_TYPE_AUTHENTICATION);
|
|
|
+ if (challenge == null) {
|
|
|
+ _logger.warn("Invalid or expired authentication challenge");
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解析认证响应数据
|
|
|
+ AuthenticationResponseData responseData = parseAuthenticationResponse(authenticationResponse);
|
|
|
+ if (responseData == null) {
|
|
|
+ _logger.warn("Failed to parse authentication response");
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取Passkey凭据
|
|
|
+ _logger.debug("Looking for passkey with credential ID: {}", responseData.credentialIdBase64);
|
|
|
+
|
|
|
+ // 先查询所有的Passkey来调试credential ID和status问题
|
|
|
+ List<UserPasskey> allPasskeys = userPasskeyService.findAll();
|
|
|
+ _logger.debug("=== CREDENTIAL ID AND STATUS DEBUG ===");
|
|
|
+ _logger.debug("Client sent credential ID: {}", responseData.credentialIdBase64);
|
|
|
+ _logger.debug("Total passkeys in database: {}", allPasskeys.size());
|
|
|
+ for (UserPasskey pk : allPasskeys) {
|
|
|
+ _logger.debug("DB credential ID: {} (user: {}, status: {})", pk.getCredentialId(), pk.getUserId(), pk.getStatus());
|
|
|
+ _logger.debug("Match check: {}", pk.getCredentialId().equals(responseData.credentialIdBase64));
|
|
|
+ }
|
|
|
+ _logger.debug("=== END CREDENTIAL ID AND STATUS DEBUG ===");
|
|
|
+
|
|
|
+ UserPasskey passkey = userPasskeyService.findByCredentialId(responseData.credentialIdBase64);
|
|
|
+ if (passkey == null) {
|
|
|
+ _logger.warn("Passkey not found for credential ID: {}", responseData.credentialIdBase64);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验证挑战与用户匹配
|
|
|
+ if (!validateChallengeUserMatch(challenge, passkey)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建服务器属性
|
|
|
+ ServerProperty serverProperty = createServerProperty(responseData.clientDataJSON, challenge.getChallenge());
|
|
|
+ if (serverProperty == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 执行WebAuthn认证验证
|
|
|
+ Map<String, Object> result = performAuthenticationVerification(responseData, passkey, serverProperty);
|
|
|
+ if (result == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 标记挑战为已使用
|
|
|
+ challenge.setStatus(1);
|
|
|
+ passkeyChallengeService.saveChallenge(challenge);
|
|
|
+
|
|
|
+ _logger.debug("Authentication verification completed successfully");
|
|
|
+ return result;
|
|
|
+
|
|
|
+ } catch (VerificationException e) {
|
|
|
+ _logger.error("WebAuthn validation failed", e);
|
|
|
+ return null;
|
|
|
+ } catch (Exception e) {
|
|
|
+ _logger.error("Error verifying authentication response", e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 认证响应数据结构
|
|
|
+ */
|
|
|
+ private static class AuthenticationResponseData {
|
|
|
+ String credentialIdBase64;
|
|
|
+ String authenticatorDataBase64;
|
|
|
+ String clientDataJSONBase64;
|
|
|
+ String signatureBase64;
|
|
|
+ String userHandleBase64;
|
|
|
+ byte[] clientDataJSON;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析认证响应
|
|
|
+ */
|
|
|
+ private AuthenticationResponseData parseAuthenticationResponse(Map<String, Object> authenticationResponse) {
|
|
|
+ String credentialIdBase64 = (String) authenticationResponse.get("credentialId");
|
|
|
+ String authenticatorDataBase64 = (String) authenticationResponse.get("authenticatorData");
|
|
|
+ String clientDataJSONBase64 = (String) authenticationResponse.get("clientDataJSON");
|
|
|
+ String signatureBase64 = (String) authenticationResponse.get("signature");
|
|
|
+ String userHandleBase64 = (String) authenticationResponse.get("userHandle");
|
|
|
+
|
|
|
+ if (credentialIdBase64 == null || authenticatorDataBase64 == null ||
|
|
|
+ clientDataJSONBase64 == null || signatureBase64 == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ _logger.info("=== AUTHENTICATION CREDENTIAL ID DEBUG ===");
|
|
|
+ _logger.info("Received credentialIdBase64 from client: {}", credentialIdBase64);
|
|
|
+ _logger.info("CredentialIdBase64 length: {}", credentialIdBase64.length());
|
|
|
+ _logger.info("=== END AUTHENTICATION CREDENTIAL ID DEBUG ===");
|
|
|
+
|
|
|
+ AuthenticationResponseData data = new AuthenticationResponseData();
|
|
|
+ data.credentialIdBase64 = credentialIdBase64;
|
|
|
+ data.authenticatorDataBase64 = authenticatorDataBase64;
|
|
|
+ data.clientDataJSONBase64 = clientDataJSONBase64;
|
|
|
+ data.signatureBase64 = signatureBase64;
|
|
|
+ data.userHandleBase64 = userHandleBase64;
|
|
|
+ data.clientDataJSON = Base64.decodeBase64(clientDataJSONBase64);
|
|
|
+
|
|
|
+ return data;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 验证挑战与用户匹配
|
|
|
+ */
|
|
|
+ private boolean validateChallengeUserMatch(PasskeyChallenge challenge, UserPasskey passkey) {
|
|
|
+ if (challenge.getUserId() != null && !challenge.getUserId().equals(passkey.getUserId())) {
|
|
|
+ _logger.warn("Challenge user mismatch: expected {}, found {}", challenge.getUserId(), passkey.getUserId());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 执行认证验证
|
|
|
+ */
|
|
|
+ private Map<String, Object> performAuthenticationVerification(AuthenticationResponseData responseData,
|
|
|
+ UserPasskey passkey, ServerProperty serverProperty) {
|
|
|
+ try {
|
|
|
+ // 解码数据
|
|
|
+ byte[] credentialId = Base64.decodeBase64(responseData.credentialIdBase64);
|
|
|
+ byte[] authenticatorData = Base64.decodeBase64(responseData.authenticatorDataBase64);
|
|
|
+ byte[] signature = Base64.decodeBase64(responseData.signatureBase64);
|
|
|
+ byte[] userHandle = responseData.userHandleBase64 != null ? Base64.decodeBase64(responseData.userHandleBase64) : null;
|
|
|
+
|
|
|
+ // 从存储的凭据中重建CredentialRecord
|
|
|
+ CredentialRecord credentialRecord = buildCredentialRecord(passkey, credentialId);
|
|
|
+ if (credentialRecord == null) {
|
|
|
+ _logger.error("Failed to build credential record");
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建认证参数
|
|
|
+ AuthenticationParameters authenticationParameters = new AuthenticationParameters(
|
|
|
+ serverProperty,
|
|
|
+ credentialRecord,
|
|
|
+ false, // userVerificationRequired
|
|
|
+ true // userPresenceRequired
|
|
|
+ );
|
|
|
+
|
|
|
+ // 更新认证参数
|
|
|
+ authenticationParameters = new AuthenticationParameters(
|
|
|
+ serverProperty,
|
|
|
+ credentialRecord,
|
|
|
+ Arrays.asList(credentialId),
|
|
|
+ false, // userVerificationRequired
|
|
|
+ true // userPresenceRequired
|
|
|
+ );
|
|
|
+
|
|
|
+ // 解析认证数据
|
|
|
+ String authenticationResponseJSON = objectConverter.getJsonConverter().writeValueAsString(
|
|
|
+ Map.of(
|
|
|
+ "id", responseData.credentialIdBase64,
|
|
|
+ "rawId", responseData.credentialIdBase64,
|
|
|
+ "response", Map.of(
|
|
|
+ "authenticatorData", responseData.authenticatorDataBase64,
|
|
|
+ "clientDataJSON", responseData.clientDataJSONBase64,
|
|
|
+ "signature", responseData.signatureBase64,
|
|
|
+ "userHandle", responseData.userHandleBase64 != null ? responseData.userHandleBase64 : ""
|
|
|
+ ),
|
|
|
+ "type", CREDENTIAL_TYPE_PUBLIC_KEY
|
|
|
+ )
|
|
|
+ );
|
|
|
+
|
|
|
+ // 使用 WebAuthnManager 解析和验证认证
|
|
|
+ AuthenticationData authenticationData = webAuthnManager.parseAuthenticationResponseJSON(authenticationResponseJSON);
|
|
|
+ webAuthnManager.verify(authenticationData, authenticationParameters);
|
|
|
+
|
|
|
+ // 验证成功,更新凭据信息
|
|
|
+ passkey.setLastUsedDate(new Date());
|
|
|
+ passkey.setSignatureCount(authenticationData.getAuthenticatorData().getSignCount());
|
|
|
+ userPasskeyService.saveOrUpdatePasskey(passkey);
|
|
|
+
|
|
|
+ // 返回认证结果
|
|
|
+ return Map.of(
|
|
|
+ "success", true,
|
|
|
+ "userId", passkey.getUserId(),
|
|
|
+ "credentialId", passkey.getCredentialId(),
|
|
|
+ "displayName", passkey.getDisplayName() != null ? passkey.getDisplayName() : "Unknown Device"
|
|
|
+ );
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ _logger.error("Authentication verification failed: {}", e.getMessage(), e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建CredentialRecord
|
|
|
+ */
|
|
|
+ private CredentialRecord buildCredentialRecord(UserPasskey passkey, byte[] credentialId) {
|
|
|
+ try {
|
|
|
+ byte[] publicKeyBytes = Base64.decodeBase64(passkey.getPublicKey());
|
|
|
+ COSEKey coseKey = objectConverter.getCborConverter().readValue(publicKeyBytes, COSEKey.class);
|
|
|
+
|
|
|
+ AttestedCredentialData attestedCredentialData = new AttestedCredentialData(
|
|
|
+ passkey.getAaguid() != null ? new AAGUID(Base64.decodeBase64(passkey.getAaguid())) : AAGUID.NULL,
|
|
|
+ credentialId,
|
|
|
+ coseKey
|
|
|
+ );
|
|
|
+
|
|
|
+ return new CredentialRecordImpl(
|
|
|
+ null, // attestationStatement
|
|
|
+ false, // uvInitialized
|
|
|
+ false, // backupEligible
|
|
|
+ false, // backupState
|
|
|
+ passkey.getSignatureCount(), // counter
|
|
|
+ attestedCredentialData, // attestedCredentialData
|
|
|
+ null, // authenticatorExtensions
|
|
|
+ null, // clientData
|
|
|
+ null, // clientExtensions
|
|
|
+ null // transports
|
|
|
+ );
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ _logger.error("Failed to build CredentialRecord: {}", e.getMessage(), e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public List<UserPasskey> getUserPasskeys(String userId) {
|
|
|
+ return userPasskeyService.findByUserId(userId);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public boolean deletePasskey(String userId, String credentialId) {
|
|
|
+ UserPasskey passkey = userPasskeyService.findByCredentialId(credentialId);
|
|
|
+ if (passkey != null && userId.equals(passkey.getUserId())) {
|
|
|
+ return userPasskeyService.deletePasskey(userId, credentialId);
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public boolean hasAnyRegisteredPasskeys() {
|
|
|
+ try {
|
|
|
+ // 通过查询所有有效用户的 Passkey 数量来判断系统中是否有注册的 Passkey
|
|
|
+ // 使用更高效的查询方法,只查询有效的 Passkey (status = 1)
|
|
|
+ List<UserPasskey> allPasskeys = userPasskeyService.findAll();
|
|
|
+ if (allPasskeys == null || allPasskeys.isEmpty()) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否有任何有效的 Passkey (status = 1)
|
|
|
+ for (UserPasskey passkey : allPasskeys) {
|
|
|
+ if (passkey.getStatus() != null && passkey.getStatus() == 1) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+ } catch (Exception e) {
|
|
|
+ _logger.error("Error checking for any registered passkeys", e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public boolean savePasskey(UserPasskey userPasskey) {
|
|
|
+ try {
|
|
|
+ if (userPasskey == null || userPasskey.getId() == null) {
|
|
|
+ _logger.warn("Cannot save null passkey or passkey with null ID");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ _logger.debug("Saving passkey: ID={}, userId={}, credentialId={}",
|
|
|
+ userPasskey.getId(), userPasskey.getUserId(), userPasskey.getCredentialId());
|
|
|
+
|
|
|
+ userPasskeyService.saveOrUpdatePasskey(userPasskey);
|
|
|
+
|
|
|
+ _logger.debug("Passkey saved successfully");
|
|
|
+ return true;
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ _logger.error("Error saving passkey: {}", e.getMessage(), e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public UserPasskey getPasskeyByCredentialId(String credentialId) {
|
|
|
+ _logger.debug("Looking for passkey with credentialId: {}", credentialId);
|
|
|
+
|
|
|
+ UserPasskey result = userPasskeyService.findByCredentialId(credentialId);
|
|
|
+
|
|
|
+ if (result != null) {
|
|
|
+ _logger.debug("Found passkey for user: {}", result.getUserId());
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ _logger.debug("No passkey found for credentialId: {}", credentialId);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public boolean updateSignatureCount(String credentialId, Long signatureCount) {
|
|
|
+ UserPasskey passkey = userPasskeyService.findByCredentialId(credentialId);
|
|
|
+ if (passkey != null) {
|
|
|
+ passkey.setSignatureCount(signatureCount);
|
|
|
+ userPasskeyService.saveOrUpdatePasskey(passkey);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public boolean saveChallenge(PasskeyChallenge challenge) {
|
|
|
+ try {
|
|
|
+ passkeyChallengeService.saveChallenge(challenge);
|
|
|
+ return true;
|
|
|
+ } catch (Exception e) {
|
|
|
+ _logger.error("Error saving challenge", e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public PasskeyChallenge getChallenge(String challengeId) {
|
|
|
+ return passkeyChallengeService.findByChallengeId(challengeId);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public int cleanExpiredChallenges() {
|
|
|
+ return passkeyChallengeService.cleanExpiredChallenges();
|
|
|
+ }
|
|
|
+}
|