From e11730266faa5de17a4aa07a366abeefed2220af Mon Sep 17 00:00:00 2001 From: seungchan Date: Wed, 27 May 2026 20:13:02 +0900 Subject: [PATCH] Add support for BINARY (BYTEA) column type in JdbcOAuth2AuthorizationService PostgreSQL uses BYTEA for binary data, which is reported by the JDBC driver as Types.BINARY (-2). The JdbcOAuth2AuthorizationService only handled Types.BLOB and Types.CLOB, causing failures when used with PostgreSQL (and compatible databases such as AWS Aurora DSQL). This commit adds handling for Types.BINARY in three places: - getLobValue: reads bytes and converts to String, trimming trailing null bytes that may be present with fixed-length BINARY columns - doSetValue: writes byte[] or String values via the LobCreator - mapToSqlParameter: converts String values to byte[] for BINARY columns Fixes gh-2321 Signed-off-by: seungchan --- .../JdbcOAuth2AuthorizationService.java | 35 ++++++++++++++++-- .../JdbcOAuth2AuthorizationServiceTests.java | 31 ++++++++++++++++ ...-authorization-schema-binary-data-type.sql | 36 +++++++++++++++++++ 3 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 oauth2-authorization-server/src/test/resources/org/springframework/security/oauth2/server/authorization/custom-oauth2-authorization-schema-binary-data-type.sql diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/JdbcOAuth2AuthorizationService.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/JdbcOAuth2AuthorizationService.java index f8f23edb5..297efeb2c 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/JdbcOAuth2AuthorizationService.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/JdbcOAuth2AuthorizationService.java @@ -453,9 +453,11 @@ private static ColumnMetadata getColumnMetadata(JdbcOperations jdbcOperations, S private static SqlParameterValue mapToSqlParameter(String columnName, String value) { ColumnMetadata columnMetadata = columnMetadataMap.get(columnName); - return (Types.BLOB == columnMetadata.getDataType() && StringUtils.hasText(value)) - ? new SqlParameterValue(Types.BLOB, value.getBytes(StandardCharsets.UTF_8)) - : new SqlParameterValue(columnMetadata.getDataType(), value); + if ((Types.BLOB == columnMetadata.getDataType() || Types.BINARY == columnMetadata.getDataType()) + && StringUtils.hasText(value)) { + return new SqlParameterValue(columnMetadata.getDataType(), value.getBytes(StandardCharsets.UTF_8)); + } + return new SqlParameterValue(columnMetadata.getDataType(), value); } /** @@ -608,6 +610,19 @@ private String getLobValue(ResultSet rs, String columnName) throws SQLException columnValue = new String(columnValueBytes, StandardCharsets.UTF_8); } } + else if (Types.BINARY == columnMetadata.getDataType()) { + byte[] columnValueBytes = this.lobHandler.getBlobAsBytes(rs, columnName); + if (columnValueBytes != null) { + // Trim trailing null bytes that may be present with fixed-length BINARY columns + int length = columnValueBytes.length; + while (length > 0 && columnValueBytes[length - 1] == 0) { + length--; + } + if (length > 0) { + columnValue = new String(columnValueBytes, 0, length, StandardCharsets.UTF_8); + } + } + } else if (Types.CLOB == columnMetadata.getDataType()) { columnValue = this.lobHandler.getClobAsString(rs, columnName); } @@ -803,6 +818,20 @@ protected void doSetValue(PreparedStatement ps, int parameterPosition, Object ar this.lobCreator.setBlobAsBytes(ps, parameterPosition, valueBytes); return; } + if (paramValue.getSqlType() == Types.BINARY) { + byte[] valueBytes = null; + if (paramValue.getValue() != null) { + Object value = paramValue.getValue(); + if (value instanceof byte[] byteArray) { + valueBytes = byteArray; + } + else if (value instanceof String stringValue) { + valueBytes = stringValue.getBytes(StandardCharsets.UTF_8); + } + } + this.lobCreator.setBlobAsBytes(ps, parameterPosition, valueBytes); + return; + } if (paramValue.getSqlType() == Types.CLOB) { if (paramValue.getValue() != null) { Assert.isInstanceOf(String.class, paramValue.getValue(), diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/JdbcOAuth2AuthorizationServiceTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/JdbcOAuth2AuthorizationServiceTests.java index 1624e876e..bf47d4ceb 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/JdbcOAuth2AuthorizationServiceTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/JdbcOAuth2AuthorizationServiceTests.java @@ -82,6 +82,8 @@ public class JdbcOAuth2AuthorizationServiceTests { private static final String OAUTH2_AUTHORIZATION_SCHEMA_CLOB_DATA_TYPE_SQL_RESOURCE = "org/springframework/security/oauth2/server/authorization/custom-oauth2-authorization-schema-clob-data-type.sql"; + private static final String OAUTH2_AUTHORIZATION_SCHEMA_BINARY_DATA_TYPE_SQL_RESOURCE = "org/springframework/security/oauth2/server/authorization/custom-oauth2-authorization-schema-binary-data-type.sql"; + private static final OAuth2TokenType AUTHORIZATION_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.CODE); private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE); @@ -520,6 +522,35 @@ public void tableDefinitionWhenClobSqlTypeThenAuthorizationUpdated() { db.shutdown(); } + @Test + public void tableDefinitionWhenBinarySqlTypeThenAuthorizationUpdated() { + given(this.registeredClientRepository.findById(eq(REGISTERED_CLIENT.getId()))).willReturn(REGISTERED_CLIENT); + + EmbeddedDatabase db = createDb(OAUTH2_AUTHORIZATION_SCHEMA_BINARY_DATA_TYPE_SQL_RESOURCE); + OAuth2AuthorizationService authorizationService = new JdbcOAuth2AuthorizationService(new JdbcTemplate(db), + this.registeredClientRepository); + OAuth2Authorization originalAuthorization = OAuth2Authorization.withRegisteredClient(REGISTERED_CLIENT) + .id(ID) + .principalName(PRINCIPAL_NAME) + .authorizationGrantType(AUTHORIZATION_GRANT_TYPE) + .token(AUTHORIZATION_CODE) + .build(); + authorizationService.save(originalAuthorization); + + OAuth2Authorization authorization = authorizationService.findById(originalAuthorization.getId()); + assertThat(authorization).isEqualTo(originalAuthorization); + + OAuth2Authorization updatedAuthorization = OAuth2Authorization.from(authorization) + .attribute("custom-name-1", "custom-value-1") + .build(); + authorizationService.save(updatedAuthorization); + + authorization = authorizationService.findById(updatedAuthorization.getId()); + assertThat(authorization).isEqualTo(updatedAuthorization); + assertThat(authorization).isNotEqualTo(originalAuthorization); + db.shutdown(); + } + private static EmbeddedDatabase createDb() { return createDb(OAUTH2_AUTHORIZATION_SCHEMA_SQL_RESOURCE); } diff --git a/oauth2-authorization-server/src/test/resources/org/springframework/security/oauth2/server/authorization/custom-oauth2-authorization-schema-binary-data-type.sql b/oauth2-authorization-server/src/test/resources/org/springframework/security/oauth2/server/authorization/custom-oauth2-authorization-schema-binary-data-type.sql new file mode 100644 index 000000000..4db0fb7f4 --- /dev/null +++ b/oauth2-authorization-server/src/test/resources/org/springframework/security/oauth2/server/authorization/custom-oauth2-authorization-schema-binary-data-type.sql @@ -0,0 +1,36 @@ +CREATE TABLE oauth2_authorization ( + id varchar(100) NOT NULL, + registered_client_id varchar(100) NOT NULL, + principal_name varchar(200) NOT NULL, + authorization_grant_type varchar(100) NOT NULL, + authorized_scopes varchar(1000) DEFAULT NULL, + attributes varchar(4000) DEFAULT NULL, + state varchar(500) DEFAULT NULL, + authorization_code_value binary(10000) DEFAULT NULL, + authorization_code_issued_at timestamp DEFAULT NULL, + authorization_code_expires_at timestamp DEFAULT NULL, + authorization_code_metadata varchar(2000) DEFAULT NULL, + access_token_value binary(10000) DEFAULT NULL, + access_token_issued_at timestamp DEFAULT NULL, + access_token_expires_at timestamp DEFAULT NULL, + access_token_metadata varchar(2000) DEFAULT NULL, + access_token_type varchar(100) DEFAULT NULL, + access_token_scopes varchar(1000) DEFAULT NULL, + oidc_id_token_value binary(10000) DEFAULT NULL, + oidc_id_token_issued_at timestamp DEFAULT NULL, + oidc_id_token_expires_at timestamp DEFAULT NULL, + oidc_id_token_metadata varchar(2000) DEFAULT NULL, + refresh_token_value binary(10000) DEFAULT NULL, + refresh_token_issued_at timestamp DEFAULT NULL, + refresh_token_expires_at timestamp DEFAULT NULL, + refresh_token_metadata varchar(2000) DEFAULT NULL, + user_code_value binary(10000) DEFAULT NULL, + user_code_issued_at timestamp DEFAULT NULL, + user_code_expires_at timestamp DEFAULT NULL, + user_code_metadata varchar(2000) DEFAULT NULL, + device_code_value binary(10000) DEFAULT NULL, + device_code_issued_at timestamp DEFAULT NULL, + device_code_expires_at timestamp DEFAULT NULL, + device_code_metadata varchar(2000) DEFAULT NULL, + PRIMARY KEY (id) +); \ No newline at end of file