Skip to content

Commit 66bc5a0

Browse files
ovidiupopa07jgrandja
authored andcommitted
Support clob and text datatype for token columns
Closes gh-480
1 parent e175f4f commit 66bc5a0

File tree

4 files changed

+205
-28
lines changed

4 files changed

+205
-28
lines changed

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/JdbcOAuth2AuthorizationService.java

Lines changed: 84 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.springframework.security.oauth2.server.authorization;
1717

1818
import java.nio.charset.StandardCharsets;
19+
import java.sql.DatabaseMetaData;
1920
import java.sql.PreparedStatement;
2021
import java.sql.ResultSet;
2122
import java.sql.SQLException;
@@ -35,6 +36,7 @@
3536

3637
import org.springframework.dao.DataRetrievalFailureException;
3738
import org.springframework.jdbc.core.ArgumentPreparedStatementSetter;
39+
import org.springframework.jdbc.core.ConnectionCallback;
3840
import org.springframework.jdbc.core.JdbcOperations;
3941
import org.springframework.jdbc.core.PreparedStatementSetter;
4042
import org.springframework.jdbc.core.RowMapper;
@@ -141,6 +143,7 @@ public class JdbcOAuth2AuthorizationService implements OAuth2AuthorizationServic
141143

142144
private final JdbcOperations jdbcOperations;
143145
private final LobHandler lobHandler;
146+
private static int tokenColumnType;
144147
private RowMapper<OAuth2Authorization> authorizationRowMapper;
145148
private Function<OAuth2Authorization, List<SqlParameterValue>> authorizationParametersMapper;
146149

@@ -169,12 +172,15 @@ public JdbcOAuth2AuthorizationService(JdbcOperations jdbcOperations,
169172
Assert.notNull(lobHandler, "lobHandler cannot be null");
170173
this.jdbcOperations = jdbcOperations;
171174
this.lobHandler = lobHandler;
175+
tokenColumnType = getColumnDataType(jdbcOperations, "access_token_value");
172176
OAuth2AuthorizationRowMapper authorizationRowMapper = new OAuth2AuthorizationRowMapper(registeredClientRepository);
173177
authorizationRowMapper.setLobHandler(lobHandler);
174178
this.authorizationRowMapper = authorizationRowMapper;
175-
this.authorizationParametersMapper = new OAuth2AuthorizationParametersMapper();
179+
OAuth2AuthorizationParametersMapper authorizationParametersMapper = new OAuth2AuthorizationParametersMapper();
180+
this.authorizationParametersMapper = authorizationParametersMapper;
176181
}
177182

183+
178184
@Override
179185
public void save(OAuth2Authorization authorization) {
180186
Assert.notNull(authorization, "authorization cannot be null");
@@ -232,26 +238,33 @@ public OAuth2Authorization findByToken(String token, @Nullable OAuth2TokenType t
232238
List<SqlParameterValue> parameters = new ArrayList<>();
233239
if (tokenType == null) {
234240
parameters.add(new SqlParameterValue(Types.VARCHAR, token));
235-
parameters.add(new SqlParameterValue(Types.BLOB, token.getBytes(StandardCharsets.UTF_8)));
236-
parameters.add(new SqlParameterValue(Types.BLOB, token.getBytes(StandardCharsets.UTF_8)));
237-
parameters.add(new SqlParameterValue(Types.BLOB, token.getBytes(StandardCharsets.UTF_8)));
241+
parameters.add(mapTokenToSqlParameter(token));
242+
parameters.add(mapTokenToSqlParameter(token));
243+
parameters.add(mapTokenToSqlParameter(token));
238244
return findBy(UNKNOWN_TOKEN_TYPE_FILTER, parameters);
239245
} else if (OAuth2ParameterNames.STATE.equals(tokenType.getValue())) {
240246
parameters.add(new SqlParameterValue(Types.VARCHAR, token));
241247
return findBy(STATE_FILTER, parameters);
242248
} else if (OAuth2ParameterNames.CODE.equals(tokenType.getValue())) {
243-
parameters.add(new SqlParameterValue(Types.BLOB, token.getBytes(StandardCharsets.UTF_8)));
249+
parameters.add(mapTokenToSqlParameter(token));
244250
return findBy(AUTHORIZATION_CODE_FILTER, parameters);
245251
} else if (OAuth2TokenType.ACCESS_TOKEN.equals(tokenType)) {
246-
parameters.add(new SqlParameterValue(Types.BLOB, token.getBytes(StandardCharsets.UTF_8)));
252+
parameters.add(mapTokenToSqlParameter(token));
247253
return findBy(ACCESS_TOKEN_FILTER, parameters);
248254
} else if (OAuth2TokenType.REFRESH_TOKEN.equals(tokenType)) {
249-
parameters.add(new SqlParameterValue(Types.BLOB, token.getBytes(StandardCharsets.UTF_8)));
255+
parameters.add(mapTokenToSqlParameter(token));
250256
return findBy(REFRESH_TOKEN_FILTER, parameters);
251257
}
252258
return null;
253259
}
254260

261+
private SqlParameterValue mapTokenToSqlParameter(String token) {
262+
if (Types.BLOB == tokenColumnType) {
263+
return new SqlParameterValue(Types.BLOB, token.getBytes(StandardCharsets.UTF_8));
264+
}
265+
return new SqlParameterValue(tokenColumnType, token);
266+
}
267+
255268
private OAuth2Authorization findBy(String filter, List<SqlParameterValue> parameters) {
256269
try (LobCreator lobCreator = getLobHandler().getLobCreator()) {
257270
PreparedStatementSetter pss = new LobCreatorArgumentPreparedStatementSetter(lobCreator,
@@ -349,25 +362,22 @@ public OAuth2Authorization mapRow(ResultSet rs, int rowNum) throws SQLException
349362
builder.attribute(OAuth2ParameterNames.STATE, state);
350363
}
351364

352-
String tokenValue;
353365
Instant tokenIssuedAt;
354366
Instant tokenExpiresAt;
355-
byte[] authorizationCodeValue = this.lobHandler.getBlobAsBytes(rs, "authorization_code_value");
367+
String authorizationCodeValue = getTokenValue(rs, "authorization_code_value");
356368

357-
if (authorizationCodeValue != null) {
358-
tokenValue = new String(authorizationCodeValue, StandardCharsets.UTF_8);
369+
if (StringUtils.hasText(authorizationCodeValue)) {
359370
tokenIssuedAt = rs.getTimestamp("authorization_code_issued_at").toInstant();
360371
tokenExpiresAt = rs.getTimestamp("authorization_code_expires_at").toInstant();
361372
Map<String, Object> authorizationCodeMetadata = parseMap(rs.getString("authorization_code_metadata"));
362373

363374
OAuth2AuthorizationCode authorizationCode = new OAuth2AuthorizationCode(
364-
tokenValue, tokenIssuedAt, tokenExpiresAt);
375+
authorizationCodeValue, tokenIssuedAt, tokenExpiresAt);
365376
builder.token(authorizationCode, (metadata) -> metadata.putAll(authorizationCodeMetadata));
366377
}
367378

368-
byte[] accessTokenValue = this.lobHandler.getBlobAsBytes(rs, "access_token_value");
369-
if (accessTokenValue != null) {
370-
tokenValue = new String(accessTokenValue, StandardCharsets.UTF_8);
379+
String accessTokenValue = getTokenValue(rs, "access_token_value");
380+
if (StringUtils.hasText(accessTokenValue)) {
371381
tokenIssuedAt = rs.getTimestamp("access_token_issued_at").toInstant();
372382
tokenExpiresAt = rs.getTimestamp("access_token_expires_at").toInstant();
373383
Map<String, Object> accessTokenMetadata = parseMap(rs.getString("access_token_metadata"));
@@ -381,25 +391,23 @@ public OAuth2Authorization mapRow(ResultSet rs, int rowNum) throws SQLException
381391
if (accessTokenScopes != null) {
382392
scopes = StringUtils.commaDelimitedListToSet(accessTokenScopes);
383393
}
384-
OAuth2AccessToken accessToken = new OAuth2AccessToken(tokenType, tokenValue, tokenIssuedAt, tokenExpiresAt, scopes);
394+
OAuth2AccessToken accessToken = new OAuth2AccessToken(tokenType, accessTokenValue, tokenIssuedAt, tokenExpiresAt, scopes);
385395
builder.token(accessToken, (metadata) -> metadata.putAll(accessTokenMetadata));
386396
}
387397

388-
byte[] oidcIdTokenValue = this.lobHandler.getBlobAsBytes(rs, "oidc_id_token_value");
389-
if (oidcIdTokenValue != null) {
390-
tokenValue = new String(oidcIdTokenValue, StandardCharsets.UTF_8);
398+
String oidcIdTokenValue = getTokenValue(rs, "oidc_id_token_value");
399+
if (StringUtils.hasText(oidcIdTokenValue)) {
391400
tokenIssuedAt = rs.getTimestamp("oidc_id_token_issued_at").toInstant();
392401
tokenExpiresAt = rs.getTimestamp("oidc_id_token_expires_at").toInstant();
393402
Map<String, Object> oidcTokenMetadata = parseMap(rs.getString("oidc_id_token_metadata"));
394403

395404
OidcIdToken oidcToken = new OidcIdToken(
396-
tokenValue, tokenIssuedAt, tokenExpiresAt, (Map<String, Object>) oidcTokenMetadata.get(OAuth2Authorization.Token.CLAIMS_METADATA_NAME));
405+
oidcIdTokenValue, tokenIssuedAt, tokenExpiresAt, (Map<String, Object>) oidcTokenMetadata.get(OAuth2Authorization.Token.CLAIMS_METADATA_NAME));
397406
builder.token(oidcToken, (metadata) -> metadata.putAll(oidcTokenMetadata));
398407
}
399408

400-
byte[] refreshTokenValue = this.lobHandler.getBlobAsBytes(rs, "refresh_token_value");
401-
if (refreshTokenValue != null) {
402-
tokenValue = new String(refreshTokenValue, StandardCharsets.UTF_8);
409+
String refreshTokenValue = getTokenValue(rs, "refresh_token_value");
410+
if (StringUtils.hasText(refreshTokenValue)) {
403411
tokenIssuedAt = rs.getTimestamp("refresh_token_issued_at").toInstant();
404412
tokenExpiresAt = null;
405413
Timestamp refreshTokenExpiresAt = rs.getTimestamp("refresh_token_expires_at");
@@ -409,12 +417,29 @@ public OAuth2Authorization mapRow(ResultSet rs, int rowNum) throws SQLException
409417
Map<String, Object> refreshTokenMetadata = parseMap(rs.getString("refresh_token_metadata"));
410418

411419
OAuth2RefreshToken refreshToken = new OAuth2RefreshToken(
412-
tokenValue, tokenIssuedAt, tokenExpiresAt);
420+
refreshTokenValue, tokenIssuedAt, tokenExpiresAt);
413421
builder.token(refreshToken, (metadata) -> metadata.putAll(refreshTokenMetadata));
414422
}
415423
return builder.build();
416424
}
417425

426+
private String getTokenValue(ResultSet rs, String tokenColumn) throws SQLException {
427+
String tokenValue = null;
428+
if (Types.CLOB == tokenColumnType) {
429+
tokenValue = this.lobHandler.getClobAsString(rs, tokenColumn);
430+
}
431+
if (Types.VARCHAR == tokenColumnType) {
432+
tokenValue = rs.getString(tokenColumn);
433+
}
434+
if (Types.BLOB == tokenColumnType) {
435+
byte[] tokenValueByte = this.lobHandler.getBlobAsBytes(rs, tokenColumn);
436+
if (tokenValueByte != null) {
437+
tokenValue = new String(tokenValueByte, StandardCharsets.UTF_8);
438+
}
439+
}
440+
return tokenValue;
441+
}
442+
418443
public final void setLobHandler(LobHandler lobHandler) {
419444
Assert.notNull(lobHandler, "lobHandler cannot be null");
420445
this.lobHandler = lobHandler;
@@ -520,12 +545,12 @@ protected final ObjectMapper getObjectMapper() {
520545

521546
private <T extends AbstractOAuth2Token> List<SqlParameterValue> toSqlParameterList(OAuth2Authorization.Token<T> token) {
522547
List<SqlParameterValue> parameters = new ArrayList<>();
523-
byte[] tokenValue = null;
548+
String tokenValue = null;
524549
Timestamp tokenIssuedAt = null;
525550
Timestamp tokenExpiresAt = null;
526551
String metadata = null;
527552
if (token != null) {
528-
tokenValue = token.getToken().getTokenValue().getBytes(StandardCharsets.UTF_8);
553+
tokenValue = token.getToken().getTokenValue();
529554
if (token.getToken().getIssuedAt() != null) {
530555
tokenIssuedAt = Timestamp.from(token.getToken().getIssuedAt());
531556
}
@@ -534,7 +559,13 @@ private <T extends AbstractOAuth2Token> List<SqlParameterValue> toSqlParameterLi
534559
}
535560
metadata = writeMap(token.getMetadata());
536561
}
537-
parameters.add(new SqlParameterValue(Types.BLOB, tokenValue));
562+
if (Types.BLOB == tokenColumnType && StringUtils.hasText(tokenValue)) {
563+
byte[] tokenValueAsBytes = tokenValue.getBytes(StandardCharsets.UTF_8);
564+
parameters.add(new SqlParameterValue(tokenColumnType, tokenValueAsBytes));
565+
} else {
566+
parameters.add(new SqlParameterValue(tokenColumnType, tokenValue));
567+
}
568+
538569
parameters.add(new SqlParameterValue(Types.TIMESTAMP, tokenIssuedAt));
539570
parameters.add(new SqlParameterValue(Types.TIMESTAMP, tokenExpiresAt));
540571
parameters.add(new SqlParameterValue(Types.VARCHAR, metadata));
@@ -551,6 +582,23 @@ private String writeMap(Map<String, Object> data) {
551582

552583
}
553584

585+
private static int getColumnDataType(JdbcOperations jdbcOperations, String columnName){
586+
return jdbcOperations.execute((ConnectionCallback<Integer>) con -> {
587+
DatabaseMetaData databaseMetaData = con.getMetaData();
588+
ResultSet rs = databaseMetaData.getColumns(null, null, TABLE_NAME, columnName);
589+
if (rs.next()) {
590+
return rs.getInt("DATA_TYPE");
591+
}
592+
// NOTE: When using HSQL: When a database object is created with one of the CREATE statements if the name is enclosed in double quotes, the exact name is used as the case-normal form.
593+
// But if it is not enclosed in double quotes, the name is converted to uppercase and this uppercase version is stored in the database as the case-normal form
594+
rs = databaseMetaData.getColumns(null, null, TABLE_NAME.toUpperCase(), columnName.toUpperCase());
595+
if (rs.next()) {
596+
return rs.getInt("DATA_TYPE");
597+
}
598+
return Types.NULL;
599+
});
600+
}
601+
554602
private static final class LobCreatorArgumentPreparedStatementSetter extends ArgumentPreparedStatementSetter {
555603
private final LobCreator lobCreator;
556604

@@ -572,6 +620,15 @@ protected void doSetValue(PreparedStatement ps, int parameterPosition, Object ar
572620
this.lobCreator.setBlobAsBytes(ps, parameterPosition, valueBytes);
573621
return;
574622
}
623+
if (paramValue.getSqlType() == Types.CLOB) {
624+
if (paramValue.getValue() != null) {
625+
Assert.isInstanceOf(String.class, paramValue.getValue(),
626+
"Value of clob parameter must be String");
627+
}
628+
String valueString = (String) paramValue.getValue();
629+
this.lobCreator.setClobAsString(ps, parameterPosition, valueString);
630+
return;
631+
}
575632
}
576633
super.doSetValue(ps, parameterPosition, argValue);
577634
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2020-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
CREATE TABLE oauth2_authorization (
17+
id varchar(100) NOT NULL,
18+
registered_client_id varchar(100) NOT NULL,
19+
principal_name varchar(200) NOT NULL,
20+
authorization_grant_type varchar(100) NOT NULL,
21+
attributes varchar(15000) DEFAULT NULL,
22+
state varchar(500) DEFAULT NULL,
23+
authorization_code_value text DEFAULT NULL,
24+
authorization_code_issued_at timestamp DEFAULT NULL,
25+
authorization_code_expires_at timestamp DEFAULT NULL,
26+
authorization_code_metadata varchar(2000) DEFAULT NULL,
27+
access_token_value text DEFAULT NULL,
28+
access_token_issued_at timestamp DEFAULT NULL,
29+
access_token_expires_at timestamp DEFAULT NULL,
30+
access_token_metadata varchar(2000) DEFAULT NULL,
31+
access_token_type varchar(100) DEFAULT NULL,
32+
access_token_scopes varchar(1000) DEFAULT NULL,
33+
oidc_id_token_value text DEFAULT NULL,
34+
oidc_id_token_issued_at timestamp DEFAULT NULL,
35+
oidc_id_token_expires_at timestamp DEFAULT NULL,
36+
oidc_id_token_metadata varchar(2000) DEFAULT NULL,
37+
refresh_token_value text DEFAULT NULL,
38+
refresh_token_issued_at timestamp DEFAULT NULL,
39+
refresh_token_expires_at timestamp DEFAULT NULL,
40+
refresh_token_metadata varchar(2000) DEFAULT NULL,
41+
PRIMARY KEY (id)
42+
);

oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/JdbcOAuth2AuthorizationServiceTests.java

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
4444
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
4545
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
46+
import org.springframework.jdbc.support.lob.DefaultLobHandler;
4647
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
4748
import org.springframework.security.oauth2.core.AuthorizationGrantType;
4849
import org.springframework.security.oauth2.core.OAuth2AccessToken;
@@ -75,6 +76,7 @@
7576
public class JdbcOAuth2AuthorizationServiceTests {
7677
private static final String OAUTH2_AUTHORIZATION_SCHEMA_SQL_RESOURCE = "org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql";
7778
private static final String CUSTOM_OAUTH2_AUTHORIZATION_SCHEMA_SQL_RESOURCE = "org/springframework/security/oauth2/server/authorization/custom-oauth2-authorization-schema.sql";
79+
private static final String OAUTH2_AUTHORIZATION_SCHEMA_CLOB_COLUMN_TYPE_SQL_RESOURCE = "org/springframework/security/oauth2/server/authorization/custom-oauth2-authorization-schema-clob-data-type.sql";
7880
private static final OAuth2TokenType AUTHORIZATION_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.CODE);
7981
private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
8082
private static final String ID = "id";
@@ -414,6 +416,37 @@ public void tableDefinitionWhenCustomThenAbleToOverride() {
414416
db.shutdown();
415417
}
416418

419+
@Test
420+
public void tableDefinitionWhenClobSqlTypeThenUpdateAuthorization() {
421+
EmbeddedDatabase db = createDb(OAUTH2_AUTHORIZATION_SCHEMA_CLOB_COLUMN_TYPE_SQL_RESOURCE);
422+
OAuth2AuthorizationService authorizationService =
423+
new JdbcOAuth2AuthorizationService(new JdbcTemplate(db), this.registeredClientRepository);
424+
when(this.registeredClientRepository.findById(eq(REGISTERED_CLIENT.getId())))
425+
.thenReturn(REGISTERED_CLIENT);
426+
OAuth2Authorization originalAuthorization = OAuth2Authorization.withRegisteredClient(REGISTERED_CLIENT)
427+
.id(ID)
428+
.principalName(PRINCIPAL_NAME)
429+
.authorizationGrantType(AUTHORIZATION_GRANT_TYPE)
430+
.token(AUTHORIZATION_CODE)
431+
.build();
432+
authorizationService.save(originalAuthorization);
433+
434+
OAuth2Authorization authorization = authorizationService.findById(
435+
originalAuthorization.getId());
436+
assertThat(authorization).isEqualTo(originalAuthorization);
437+
438+
OAuth2Authorization updatedAuthorization = OAuth2Authorization.from(authorization)
439+
.attribute("custom-name-1", "custom-value-1")
440+
.build();
441+
authorizationService.save(updatedAuthorization);
442+
443+
authorization = authorizationService.findById(
444+
updatedAuthorization.getId());
445+
assertThat(authorization).isEqualTo(updatedAuthorization);
446+
assertThat(authorization).isNotEqualTo(originalAuthorization);
447+
db.shutdown();
448+
}
449+
417450
private static EmbeddedDatabase createDb() {
418451
return createDb(OAUTH2_AUTHORIZATION_SCHEMA_SQL_RESOURCE);
419452
}
@@ -479,11 +512,14 @@ private static final class CustomJdbcOAuth2AuthorizationService extends JdbcOAut
479512

480513
private CustomJdbcOAuth2AuthorizationService(JdbcOperations jdbcOperations,
481514
RegisteredClientRepository registeredClientRepository) {
482-
super(jdbcOperations, registeredClientRepository);
515+
super(jdbcOperations, registeredClientRepository, new DefaultLobHandler());
483516
setAuthorizationRowMapper(new CustomOAuth2AuthorizationRowMapper(registeredClientRepository));
484517
setAuthorizationParametersMapper(new CustomOAuth2AuthorizationParametersMapper());
518+
485519
}
486520

521+
522+
487523
@Override
488524
public void save(OAuth2Authorization authorization) {
489525
List<SqlParameterValue> parameters = getAuthorizationParametersMapper().apply(authorization);

0 commit comments

Comments
 (0)