Skip to content

Commit 5842990

Browse files
committed
JDBC implementation of RegisteredClientRepository
1 parent 5e0fe9c commit 5842990

File tree

5 files changed

+561
-0
lines changed

5 files changed

+561
-0
lines changed

gradle/dependency-management.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,6 @@ dependencyManagement {
3232
dependency "com.squareup.okhttp3:mockwebserver:3.14.9"
3333
dependency "com.squareup.okhttp3:okhttp:3.14.9"
3434
dependency "com.jayway.jsonpath:json-path:2.4.0"
35+
dependency "org.hsqldb:hsqldb:2.5.+"
3536
}
3637
}

oauth2-authorization-server/spring-security-oauth2-authorization-server.gradle

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,18 @@ dependencies {
1010
compile 'com.nimbusds:nimbus-jose-jwt'
1111
compile 'com.fasterxml.jackson.core:jackson-databind'
1212

13+
optional 'org.springframework:spring-jdbc'
14+
optional 'org.springframework:spring-tx'
15+
1316
testCompile 'org.springframework.security:spring-security-test'
1417
testCompile 'org.springframework:spring-webmvc'
1518
testCompile 'junit:junit'
1619
testCompile 'org.assertj:assertj-core'
1720
testCompile 'org.mockito:mockito-core'
1821
testCompile 'com.jayway.jsonpath:json-path'
1922

23+
testRuntime 'org.hsqldb:hsqldb'
24+
2025
provided 'javax.servlet:javax.servlet-api'
2126
}
2227

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
/*
2+
* Copyright 2020-2021 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+
package org.springframework.security.oauth2.server.authorization.client;
17+
18+
import org.springframework.jdbc.core.*;
19+
import org.springframework.security.oauth2.core.AuthorizationGrantType;
20+
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
21+
import org.springframework.security.oauth2.server.authorization.config.ClientSettings;
22+
import org.springframework.security.oauth2.server.authorization.config.TokenSettings;
23+
import org.springframework.util.Assert;
24+
25+
import java.sql.ResultSet;
26+
import java.sql.SQLException;
27+
import java.sql.Timestamp;
28+
import java.sql.Types;
29+
import java.time.Duration;
30+
import java.util.*;
31+
import java.util.function.Function;
32+
import java.util.stream.Collectors;
33+
34+
/**
35+
* JDBC-backed registered client repository
36+
*
37+
* @author Rafal Lewczuk
38+
* @since 0.1.2
39+
*/
40+
public class JdbcRegisteredClientRepository implements RegisteredClientRepository {
41+
42+
private static final Map<String, AuthorizationGrantType> AUTHORIZATION_GRANT_TYPE_MAP;
43+
private static final Map<String, ClientAuthenticationMethod> CLIENT_AUTHENTICATION_METHOD_MAP;
44+
45+
private static final String COLUMN_NAMES = "id, "
46+
+ "client_id, "
47+
+ "client_id_issued_at, "
48+
+ "client_secret, "
49+
+ "client_secret_expires_at, "
50+
+ "client_name, "
51+
+ "client_authentication_methods, "
52+
+ "authorization_grant_types, "
53+
+ "redirect_uris, "
54+
+ "scopes, "
55+
+ "require_proof_key, "
56+
+ "require_user_consent, "
57+
+ "access_token_ttl, "
58+
+ "reuse_refresh_tokens, "
59+
+ "refresh_token_ttl";
60+
61+
private static final String TABLE_NAME = "oauth2_registered_client";
62+
63+
private static final String LOAD_REGISTERED_CLIENT_SQL = "SELECT " + COLUMN_NAMES + " FROM " + TABLE_NAME + " WHERE ";
64+
65+
private static final String INSERT_REGISTERED_CLIENT_SQL = "INSERT INTO " + TABLE_NAME
66+
+ "(" + COLUMN_NAMES + ") values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
67+
68+
protected RowMapper<RegisteredClient> registeredClientRowMapper;
69+
70+
protected Function<RegisteredClient, List<SqlParameterValue>> registeredClientParameterMapper;
71+
72+
protected Function<String, Collection<String>> listParser;
73+
74+
protected Function<Collection<String>, String> listFormatter;
75+
76+
protected JdbcOperations jdbcOperations;
77+
78+
public JdbcRegisteredClientRepository(JdbcOperations jdbcOperations) {
79+
Assert.notNull(jdbcOperations, "jdbcOperations cannot be null");
80+
this.jdbcOperations = jdbcOperations;
81+
this.registeredClientRowMapper = this::defaultRegisteredClientRowMapper;
82+
this.registeredClientParameterMapper = this::defaultRegisteredClientParameterMapper;
83+
this.listParser = this::defaultListParse;
84+
this.listFormatter = this::defaultListFormat;
85+
}
86+
87+
/**
88+
* Allows changing of {@link RegisteredClient} row mapper implementation
89+
*
90+
* @param registeredClientRowMapper mapper implementation
91+
*/
92+
public void setRegisteredClientRowMapper(RowMapper<RegisteredClient> registeredClientRowMapper) {
93+
Assert.notNull(registeredClientRowMapper, "registeredClientRowMapper cannot be null");
94+
this.registeredClientRowMapper = registeredClientRowMapper;
95+
}
96+
97+
/**
98+
* Allows changing of SQL parameter mapper for {@link RegisteredClient}
99+
*
100+
* @param registeredClientParameterMapper mapper implementation
101+
*/
102+
public void setRegisteredClientParameterMapper(Function<RegisteredClient, List<SqlParameterValue>> registeredClientParameterMapper) {
103+
Assert.notNull(registeredClientParameterMapper, "registeredClientParameterMapper cannot be null");
104+
this.registeredClientParameterMapper = registeredClientParameterMapper;
105+
}
106+
107+
/**
108+
* Allows changing format of list attributes of {@link RegisteredClient}
109+
*
110+
* @param listParser new list parser function
111+
*/
112+
public void setListParser(Function<String, Collection<String>> listParser) {
113+
Assert.notNull(listParser, "listParser cannot be null");
114+
this.listParser = listParser;
115+
}
116+
117+
/**
118+
* Allows changing format of list attributes of {@link RegisteredClient}
119+
*
120+
* @param listFormatter new list formatter function
121+
*/
122+
public void setListFormatter(Function<Collection<String>, String> listFormatter) {
123+
Assert.notNull(listFormatter, "listFormatter cannot be null");
124+
this.listFormatter = listFormatter;
125+
}
126+
127+
@Override
128+
public void save(RegisteredClient registeredClient) {
129+
Assert.notNull(registeredClient, "registeredClient cannot be null");
130+
RegisteredClient foundClient = this.findBy("id = ? OR client_id = ? OR client_secret = ?",
131+
registeredClient.getId(), registeredClient.getClientId(), registeredClient.getClientSecret());
132+
133+
if (null != foundClient) {
134+
Assert.isTrue(!foundClient.getId().equals(registeredClient.getId()),
135+
"Registered client must be unique. Found duplicate identifier: " + registeredClient.getId());
136+
Assert.isTrue(!foundClient.getClientId().equals(registeredClient.getClientId()),
137+
"Registered client must be unique. Found duplicate client identifier: " + registeredClient.getClientId());
138+
Assert.isTrue(!foundClient.getClientSecret().equals(registeredClient.getClientSecret()),
139+
"Registered client must be unique. Found duplicate client secret for identifier: " + registeredClient.getId());
140+
}
141+
142+
List<SqlParameterValue> parameters = this.registeredClientParameterMapper.apply(registeredClient);
143+
PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray());
144+
jdbcOperations.update(INSERT_REGISTERED_CLIENT_SQL, pss);
145+
}
146+
147+
@Override
148+
public RegisteredClient findById(String id) {
149+
Assert.hasText(id, "id cannot be empty");
150+
return findBy("id = ?", id);
151+
}
152+
153+
@Override
154+
public RegisteredClient findByClientId(String clientId) {
155+
Assert.hasText(clientId, "clientId cannot be empty");
156+
return findBy("client_id = ?", clientId);
157+
}
158+
159+
private RegisteredClient findBy(String condStr, Object...args) {
160+
List<RegisteredClient> lst = jdbcOperations.query(
161+
LOAD_REGISTERED_CLIENT_SQL + condStr,
162+
registeredClientRowMapper, args);
163+
return lst.size() == 1 ? lst.get(0) : null;
164+
}
165+
166+
private RegisteredClient defaultRegisteredClientRowMapper(ResultSet rs, int rownum) throws SQLException {
167+
Collection<String> scopes = listParser.apply(rs.getString("scopes"));
168+
List<AuthorizationGrantType> authGrantTypes = listParser.apply(rs.getString("authorization_grant_types"))
169+
.stream().map(AUTHORIZATION_GRANT_TYPE_MAP::get).collect(Collectors.toList());
170+
List<ClientAuthenticationMethod> clientAuthMethods = listParser.apply(rs.getString("client_authentication_methods"))
171+
.stream().map(CLIENT_AUTHENTICATION_METHOD_MAP::get).collect(Collectors.toList());
172+
Collection<String> redirectUris = listParser.apply(rs.getString("redirect_uris"));
173+
Timestamp clientIssuedAt = rs.getTimestamp("client_id_issued_at");
174+
Timestamp clientSecretExpiresAt = rs.getTimestamp("client_secret_expires_at");
175+
RegisteredClient.Builder builder = RegisteredClient
176+
.withId(rs.getString("id"))
177+
.clientId(rs.getString("client_id"))
178+
.clientIdIssuedAt(clientIssuedAt != null ? clientIssuedAt.toInstant() : null)
179+
.clientSecret(rs.getString("client_secret"))
180+
.clientSecretExpiresAt(clientSecretExpiresAt != null ? clientSecretExpiresAt.toInstant() : null)
181+
.clientName(rs.getString("client_name"))
182+
.clientAuthenticationMethods(coll -> coll.addAll(clientAuthMethods))
183+
.authorizationGrantTypes(coll -> coll.addAll(authGrantTypes))
184+
.redirectUris(coll -> coll.addAll(redirectUris))
185+
.scopes(coll -> coll.addAll(scopes));
186+
187+
RegisteredClient rc = builder.build();
188+
189+
TokenSettings ts = rc.getTokenSettings();
190+
ts.accessTokenTimeToLive(Duration.ofMillis(rs.getLong("access_token_ttl")));
191+
ts.refreshTokenTimeToLive(Duration.ofMillis(rs.getLong("refresh_token_ttl")));
192+
ts.reuseRefreshTokens(rs.getBoolean("reuse_refresh_tokens"));
193+
194+
ClientSettings cs = rc.getClientSettings();
195+
cs.requireProofKey(rs.getBoolean("require_proof_key"));
196+
cs.requireUserConsent(rs.getBoolean("require_user_consent"));
197+
198+
return rc;
199+
}
200+
201+
private List<SqlParameterValue> defaultRegisteredClientParameterMapper(RegisteredClient registeredClient) {
202+
List<String> clientAuthenticationMethodNames = registeredClient.getClientAuthenticationMethods().stream()
203+
.map(ClientAuthenticationMethod::getValue).collect(Collectors.toList());
204+
List<String> authorizationGrantTypeNames = registeredClient.getAuthorizationGrantTypes().stream()
205+
.map(AuthorizationGrantType::getValue).collect(Collectors.toList());
206+
Timestamp clientIdIssuedAt = registeredClient.getClientIdIssuedAt() != null ?
207+
Timestamp.from(registeredClient.getClientIdIssuedAt()) : null;
208+
Timestamp clientSecretExpiresAt = registeredClient.getClientSecretExpiresAt() != null ?
209+
Timestamp.from(registeredClient.getClientSecretExpiresAt()) : null;
210+
return Arrays.asList(
211+
new SqlParameterValue(Types.VARCHAR, registeredClient.getId()),
212+
new SqlParameterValue(Types.VARCHAR, registeredClient.getClientId()),
213+
new SqlParameterValue(Types.TIMESTAMP, clientIdIssuedAt),
214+
new SqlParameterValue(Types.VARCHAR, registeredClient.getClientSecret()),
215+
new SqlParameterValue(Types.TIMESTAMP, clientSecretExpiresAt),
216+
new SqlParameterValue(Types.VARCHAR, registeredClient.getClientName()),
217+
new SqlParameterValue(Types.VARCHAR, listFormatter.apply(clientAuthenticationMethodNames)),
218+
new SqlParameterValue(Types.VARCHAR, listFormatter.apply(authorizationGrantTypeNames)),
219+
new SqlParameterValue(Types.VARCHAR, listFormatter.apply(registeredClient.getRedirectUris())),
220+
new SqlParameterValue(Types.VARCHAR, listFormatter.apply(registeredClient.getScopes())),
221+
new SqlParameterValue(Types.BOOLEAN, registeredClient.getClientSettings().requireProofKey()),
222+
new SqlParameterValue(Types.BOOLEAN, registeredClient.getClientSettings().requireUserConsent()),
223+
new SqlParameterValue(Types.NUMERIC, registeredClient.getTokenSettings().accessTokenTimeToLive().toMillis()),
224+
new SqlParameterValue(Types.BOOLEAN, registeredClient.getTokenSettings().reuseRefreshTokens()),
225+
new SqlParameterValue(Types.NUMERIC, registeredClient.getTokenSettings().refreshTokenTimeToLive().toMillis()));
226+
}
227+
228+
private Collection<String> defaultListParse(String s) {
229+
return s != null ? Arrays.asList(s.split("\\|")) : Collections.emptyList();
230+
}
231+
232+
private String defaultListFormat(Collection<String> items) {
233+
return String.join("|", items);
234+
}
235+
236+
static {
237+
Map<String, AuthorizationGrantType> am = new HashMap<>();
238+
for (AuthorizationGrantType a : Arrays.asList(
239+
AuthorizationGrantType.AUTHORIZATION_CODE,
240+
AuthorizationGrantType.REFRESH_TOKEN,
241+
AuthorizationGrantType.CLIENT_CREDENTIALS,
242+
AuthorizationGrantType.PASSWORD,
243+
AuthorizationGrantType.IMPLICIT)) {
244+
am.put(a.getValue(), a);
245+
}
246+
AUTHORIZATION_GRANT_TYPE_MAP = Collections.unmodifiableMap(am);
247+
248+
Map<String, ClientAuthenticationMethod> cm = new HashMap<>();
249+
for (ClientAuthenticationMethod c : Arrays.asList(
250+
ClientAuthenticationMethod.NONE,
251+
ClientAuthenticationMethod.BASIC,
252+
ClientAuthenticationMethod.POST)) {
253+
cm.put(c.getValue(), c);
254+
}
255+
CLIENT_AUTHENTICATION_METHOD_MAP = Collections.unmodifiableMap(cm);
256+
}
257+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
create table oauth2_registered_client (
2+
id varchar(128) primary key not null,
3+
client_id varchar(128) not null,
4+
client_id_issued_at datetime,
5+
client_secret varchar(4096) not null,
6+
client_secret_expires_at datetime,
7+
client_name varchar(512),
8+
client_authentication_methods varchar(512) not null,
9+
authorization_grant_types varchar(512) not null,
10+
redirect_uris varchar(4096) not null,
11+
scopes varchar(1024) not null,
12+
require_proof_key boolean not null,
13+
require_user_consent boolean not null,
14+
access_token_ttl integer default 300000 not null,
15+
reuse_refresh_tokens boolean default true not null,
16+
refresh_token_ttl integer default 600000 not null);

0 commit comments

Comments
 (0)