Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix for JdbcOneTimeTokenService cleanupExpiredTokens failing with PostgreSQL #16344

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

making
Copy link
Member

@making making commented Dec 25, 2024

JdbcOneTimeTokenService's cleanupExpiredTokens does not work with some databases, such as PostgreSQL. The following exception is thrown. This is because the JDBC Driver does not support mapping java.time.Instant to timestamp.

In this PR, instead of binding Instat directly to the parameter, it is converted to Timestamp first and then bound.

2024-12-26T01:00:00.025+09:00 DEBUG 8864 --- [demo-magic-link] [e-time-tokens-1] [                                                 ] o.s.jdbc.core.JdbcTemplate               : Executing prepared SQL update
2024-12-26T01:00:00.026+09:00 DEBUG 8864 --- [demo-magic-link] [e-time-tokens-1] [                                                 ] o.s.jdbc.core.JdbcTemplate               : Executing prepared SQL statement [DELETE FROM one_time_tokens WHERE expires_at < ?]
2024-12-26T01:00:00.039+09:00 ERROR 8864 --- [demo-magic-link] [e-time-tokens-1] [                                                 ] o.s.s.s.TaskUtils$LoggingErrorHandler    : Unexpected error occurred in scheduled task

org.springframework.dao.DataIntegrityViolationException: PreparedStatementCallback; SQL [DELETE FROM one_time_tokens WHERE expires_at < ?]; Bad value for type timestamp/date/time: 2024-12-25T16:00:00.025429Z
	at org.springframework.jdbc.support.SQLStateSQLExceptionTranslator.doTranslate(SQLStateSQLExceptionTranslator.java:118) ~[spring-jdbc-6.2.1.jar:6.2.1]
	at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:107) ~[spring-jdbc-6.2.1.jar:6.2.1]
	at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:116) ~[spring-jdbc-6.2.1.jar:6.2.1]
	at org.springframework.jdbc.core.JdbcTemplate.translateException(JdbcTemplate.java:1556) ~[spring-jdbc-6.2.1.jar:6.2.1]
	at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:677) ~[spring-jdbc-6.2.1.jar:6.2.1]
	at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:972) ~[spring-jdbc-6.2.1.jar:6.2.1]
	at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:1016) ~[spring-jdbc-6.2.1.jar:6.2.1]
	at org.springframework.security.authentication.ott.JdbcOneTimeTokenService.cleanupExpiredTokens(JdbcOneTimeTokenService.java:195) ~[spring-security-core-6.4.2.jar:6.4.2]
	at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) ~[spring-context-6.2.1.jar:6.2.1]
	at org.springframework.scheduling.concurrent.ReschedulingRunnable.run(ReschedulingRunnable.java:96) ~[spring-context-6.2.1.jar:6.2.1]
	at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572) ~[na:na]
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317) ~[na:na]
	at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304) ~[na:na]
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) ~[na:na]
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) ~[na:na]
	at java.base/java.lang.Thread.run(Thread.java:1583) ~[na:na]
Caused by: org.postgresql.util.PSQLException: Bad value for type timestamp/date/time: 2024-12-25T16:00:00.025429Z
	at org.postgresql.jdbc.TimestampUtils.parseBackendTimestamp(TimestampUtils.java:379) ~[postgresql-42.7.4.jar:42.7.4]
	at org.postgresql.jdbc.TimestampUtils.toTimestamp(TimestampUtils.java:459) ~[postgresql-42.7.4.jar:42.7.4]
	at org.postgresql.jdbc.PgPreparedStatement.setObject(PgPreparedStatement.java:674) ~[postgresql-42.7.4.jar:42.7.4]
	at org.postgresql.jdbc.PgPreparedStatement.setObject(PgPreparedStatement.java:987) ~[postgresql-42.7.4.jar:42.7.4]
	at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.setObject(HikariProxyPreparedStatement.java) ~[HikariCP-5.1.0.jar:na]
	at org.springframework.jdbc.core.StatementCreatorUtils.setValue(StatementCreatorUtils.java:434) ~[spring-jdbc-6.2.1.jar:6.2.1]
	at org.springframework.jdbc.core.StatementCreatorUtils.setParameterValueInternal(StatementCreatorUtils.java:247) ~[spring-jdbc-6.2.1.jar:6.2.1]
	at org.springframework.jdbc.core.StatementCreatorUtils.setParameterValue(StatementCreatorUtils.java:163) ~[spring-jdbc-6.2.1.jar:6.2.1]
	at org.springframework.jdbc.core.ArgumentPreparedStatementSetter.doSetValue(ArgumentPreparedStatementSetter.java:69) ~[spring-jdbc-6.2.1.jar:6.2.1]
	at org.springframework.jdbc.core.ArgumentPreparedStatementSetter.setValues(ArgumentPreparedStatementSetter.java:51) ~[spring-jdbc-6.2.1.jar:6.2.1]
	at org.springframework.jdbc.core.JdbcTemplate.lambda$update$2(JdbcTemplate.java:975) ~[spring-jdbc-6.2.1.jar:6.2.1]
	at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:658) ~[spring-jdbc-6.2.1.jar:6.2.1]
	... 11 common frames omitted
Caused by: java.lang.NumberFormatException: Trailing junk on timestamp: 'T16:00:00.025429Z'
	at org.postgresql.jdbc.TimestampUtils.parseBackendTimestamp(TimestampUtils.java:369) ~[postgresql-42.7.4.jar:42.7.4]
	... 22 common frames omitted

This PR does not add tests using PostgreSQL, but I used TestContainers to check the behavior with the following code.

package com.example;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.authentication.ott.JdbcOneTimeTokenService;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.utility.DockerImageName;

@JdbcTest
public class JdbcOneTimeTokenServiceTest {

	@Test
	void test(@Autowired JdbcOneTimeTokenService oneTimeTokenService) {
		oneTimeTokenService.cleanupExpiredTokens();
	}

	@TestConfiguration
	static class Config {

		@Bean
		JdbcOneTimeTokenService jdbcOneTimeTokenService(JdbcTemplate jdbcTemplate) {
			return new JdbcOneTimeTokenService(jdbcTemplate);
		}

		@Bean
		@ServiceConnection
		PostgreSQLContainer<?> postgresContainer() {
			return new PostgreSQLContainer<>(DockerImageName.parse("postgres:16-alpine"));
		}

	}

}
-- schema.sql
CREATE TABLE IF NOT EXISTS one_time_tokens
(
    token_value VARCHAR(36)  NOT NULL PRIMARY KEY,
    username    VARCHAR(128) NOT NULL,
    expires_at  TIMESTAMP    NOT NULL
);

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Dec 25, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: waiting-for-triage An issue we've not yet triaged
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants