From 2f4160323647ed4728179508d7c702c51eb9a7a1 Mon Sep 17 00:00:00 2001 From: Max Batischev Date: Wed, 19 Feb 2025 14:43:44 +0300 Subject: [PATCH] Fix JdbcUserCredentialRepository Save Closes gh-16620 Signed-off-by: Max Batischev --- .../JdbcUserCredentialRepository.java | 47 ++++++++++++++++++- .../JdbcUserCredentialRepositoryTests.java | 24 +++++++++- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/web/src/main/java/org/springframework/security/web/webauthn/management/JdbcUserCredentialRepository.java b/web/src/main/java/org/springframework/security/web/webauthn/management/JdbcUserCredentialRepository.java index aa012d6964b..0ff14fcff59 100644 --- a/web/src/main/java/org/springframework/security/web/webauthn/management/JdbcUserCredentialRepository.java +++ b/web/src/main/java/org/springframework/security/web/webauthn/management/JdbcUserCredentialRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ import java.util.Set; import java.util.function.Function; +import org.springframework.dao.DuplicateKeyException; import org.springframework.jdbc.core.ArgumentPreparedStatementSetter; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.PreparedStatementSetter; @@ -112,6 +113,24 @@ public final class JdbcUserCredentialRepository implements UserCredentialReposit private static final String DELETE_CREDENTIAL_RECORD_SQL = "DELETE FROM " + TABLE_NAME + " WHERE " + ID_FILTER; + // @formatter:off + private static final String UPDATE_CREDENTIAL_RECORD_SQL = "UPDATE " + TABLE_NAME + + " SET user_entity_user_id = ?, " + + "public_key = ?, " + + "signature_count = ?, " + + "uv_initialized = ?, " + + "backup_eligible = ? ," + + "authenticator_transports = ?, " + + "public_key_credential_type = ?, " + + "backup_state = ?, " + + "attestation_object = ?, " + + "attestation_client_data_json = ?, " + + "created = ?, " + + "last_used = ?, " + + "label = ?" + + " WHERE " + ID_FILTER; + // @formatter:on + /** * Constructs a {@code JdbcUserCredentialRepository} using the provided parameters. * @param jdbcOperations the JDBC operations @@ -133,6 +152,21 @@ public void delete(Bytes credentialId) { @Override public void save(CredentialRecord record) { Assert.notNull(record, "record cannot be null"); + boolean existsRecord = null != this.findByCredentialId(record.getCredentialId()); + if (existsRecord) { + updateCredentialRecord(record); + } + else { + try { + insertCredentialRecord(record); + } + catch (DuplicateKeyException ex) { + updateCredentialRecord(record); + } + } + } + + private void insertCredentialRecord(CredentialRecord record) { List parameters = this.credentialRecordParametersMapper.apply(record); try (LobCreator lobCreator = this.lobHandler.getLobCreator()) { PreparedStatementSetter pss = new LobCreatorArgumentPreparedStatementSetter(lobCreator, @@ -141,6 +175,17 @@ public void save(CredentialRecord record) { } } + private void updateCredentialRecord(CredentialRecord record) { + List parameters = this.credentialRecordParametersMapper.apply(record); + SqlParameterValue credentialId = parameters.remove(0); + parameters.add(credentialId); + try (LobCreator lobCreator = this.lobHandler.getLobCreator()) { + PreparedStatementSetter pss = new LobCreatorArgumentPreparedStatementSetter(lobCreator, + parameters.toArray()); + this.jdbcOperations.update(UPDATE_CREDENTIAL_RECORD_SQL, pss); + } + } + @Override public CredentialRecord findByCredentialId(Bytes credentialId) { Assert.notNull(credentialId, "credentialId cannot be null"); diff --git a/web/src/test/java/org/springframework/security/web/webauthn/management/JdbcUserCredentialRepositoryTests.java b/web/src/test/java/org/springframework/security/web/webauthn/management/JdbcUserCredentialRepositoryTests.java index 4829b537f0a..3f495c2455b 100644 --- a/web/src/test/java/org/springframework/security/web/webauthn/management/JdbcUserCredentialRepositoryTests.java +++ b/web/src/test/java/org/springframework/security/web/webauthn/management/JdbcUserCredentialRepositoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; import org.springframework.security.web.webauthn.api.AuthenticatorTransport; import org.springframework.security.web.webauthn.api.CredentialRecord; +import org.springframework.security.web.webauthn.api.ImmutableCredentialRecord; import org.springframework.security.web.webauthn.api.PublicKeyCredentialType; import org.springframework.security.web.webauthn.api.TestCredentialRecord; @@ -133,6 +134,27 @@ void saveCredentialRecordWhenSaveThenReturnsSaved() { assertThat(new String(savedUserCredential.getAttestationClientDataJSON().getBytes())).isEqualTo("test"); } + @Test + void saveCredentialRecordWhenRecordExistsThenReturnsUpdated() { + CredentialRecord userCredential = TestCredentialRecord.fullUserCredential().build(); + this.jdbcUserCredentialRepository.save(userCredential); + // @formatter:off + CredentialRecord updatedRecord = ImmutableCredentialRecord.fromCredentialRecord(userCredential) + .backupEligible(false) + .uvInitialized(true) + .signatureCount(200).build(); + // @formatter:on + + this.jdbcUserCredentialRepository.save(updatedRecord); + + CredentialRecord record = this.jdbcUserCredentialRepository + .findByCredentialId(userCredential.getCredentialId()); + + assertThat(record.getSignatureCount()).isEqualTo(200); + assertThat(record.isUvInitialized()).isTrue(); + assertThat(record.isBackupEligible()).isFalse(); + } + @Test void findCredentialRecordByUserIdWhenRecordExistsThenReturnsSaved() { CredentialRecord userCredential = TestCredentialRecord.fullUserCredential().build();