1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.springframework.security.oauth2.provider.client;
18
19 import java.sql.ResultSet;
20 import java.sql.SQLException;
21 import java.util.Collections;
22 import java.util.HashSet;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Set;
26 import javax.sql.DataSource;
27
28 import com.fasterxml.jackson.databind.ObjectMapper;
29 import org.apache.commons.logging.Log;
30 import org.apache.commons.logging.LogFactory;
31
32 import org.springframework.dao.DuplicateKeyException;
33 import org.springframework.dao.EmptyResultDataAccessException;
34 import org.springframework.jdbc.core.JdbcTemplate;
35 import org.springframework.jdbc.core.RowMapper;
36 import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
37 import org.springframework.security.crypto.password.NoOpPasswordEncoder;
38 import org.springframework.security.crypto.password.PasswordEncoder;
39 import org.springframework.security.oauth2.common.exceptions.InvalidClientException;
40 import org.springframework.security.oauth2.common.util.DefaultJdbcListFactory;
41 import org.springframework.security.oauth2.common.util.JdbcListFactory;
42 import org.springframework.security.oauth2.provider.ClientAlreadyExistsException;
43 import org.springframework.security.oauth2.provider.ClientDetails;
44 import org.springframework.security.oauth2.provider.ClientDetailsService;
45 import org.springframework.security.oauth2.provider.ClientRegistrationService;
46 import org.springframework.security.oauth2.provider.NoSuchClientException;
47 import org.springframework.util.Assert;
48 import org.springframework.util.ClassUtils;
49 import org.springframework.util.StringUtils;
50
51
52
53
54 public class JdbcClientDetailsService implements ClientDetailsService, ClientRegistrationService {
55
56 private static final Log logger = LogFactory.getLog(JdbcClientDetailsService.class);
57
58 private JsonMapper mapper = createJsonMapper();
59
60 private static final String CLIENT_FIELDS_FOR_UPDATE = "resource_ids, scope, "
61 + "authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, "
62 + "refresh_token_validity, additional_information, autoapprove";
63
64 private static final String CLIENT_FIELDS = "client_secret, " + CLIENT_FIELDS_FOR_UPDATE;
65
66 private static final String BASE_FIND_STATEMENT = "select client_id, " + CLIENT_FIELDS
67 + " from oauth_client_details";
68
69 private static final String DEFAULT_FIND_STATEMENT = BASE_FIND_STATEMENT + " order by client_id";
70
71 private static final String DEFAULT_SELECT_STATEMENT = BASE_FIND_STATEMENT + " where client_id = ?";
72
73 private static final String DEFAULT_INSERT_STATEMENT = "insert into oauth_client_details (" + CLIENT_FIELDS
74 + ", client_id) values (?,?,?,?,?,?,?,?,?,?,?)";
75
76 private static final String DEFAULT_UPDATE_STATEMENT = "update oauth_client_details " + "set "
77 + CLIENT_FIELDS_FOR_UPDATE.replaceAll(", ", "=?, ") + "=? where client_id = ?";
78
79 private static final String DEFAULT_UPDATE_SECRET_STATEMENT = "update oauth_client_details "
80 + "set client_secret = ? where client_id = ?";
81
82 private static final String DEFAULT_DELETE_STATEMENT = "delete from oauth_client_details where client_id = ?";
83
84 private RowMapper<ClientDetails> rowMapper = new ClientDetailsRowMapper();
85
86 private String deleteClientDetailsSql = DEFAULT_DELETE_STATEMENT;
87
88 private String findClientDetailsSql = DEFAULT_FIND_STATEMENT;
89
90 private String updateClientDetailsSql = DEFAULT_UPDATE_STATEMENT;
91
92 private String updateClientSecretSql = DEFAULT_UPDATE_SECRET_STATEMENT;
93
94 private String insertClientDetailsSql = DEFAULT_INSERT_STATEMENT;
95
96 private String selectClientDetailsSql = DEFAULT_SELECT_STATEMENT;
97
98 private PasswordEncoder passwordEncoder = NoOpPasswordEncoder.getInstance();
99
100 private final JdbcTemplate jdbcTemplate;
101
102 private JdbcListFactory listFactory;
103
104 public JdbcClientDetailsService(DataSource dataSource) {
105 Assert.notNull(dataSource, "DataSource required");
106 this.jdbcTemplate = new JdbcTemplate(dataSource);
107 this.listFactory = new DefaultJdbcListFactory(new NamedParameterJdbcTemplate(jdbcTemplate));
108 }
109
110
111
112
113 public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
114 this.passwordEncoder = passwordEncoder;
115 }
116
117 public ClientDetails loadClientByClientId(String clientId) throws InvalidClientException {
118 ClientDetails details;
119 try {
120 details = jdbcTemplate.queryForObject(selectClientDetailsSql, new ClientDetailsRowMapper(), clientId);
121 }
122 catch (EmptyResultDataAccessException e) {
123 throw new NoSuchClientException("No client with requested id: " + clientId);
124 }
125
126 return details;
127 }
128
129 public void addClientDetails(ClientDetails clientDetails) throws ClientAlreadyExistsException {
130 try {
131 jdbcTemplate.update(insertClientDetailsSql, getFields(clientDetails));
132 }
133 catch (DuplicateKeyException e) {
134 throw new ClientAlreadyExistsException("Client already exists: " + clientDetails.getClientId(), e);
135 }
136 }
137
138 public void updateClientDetails(ClientDetails clientDetails) throws NoSuchClientException {
139 int count = jdbcTemplate.update(updateClientDetailsSql, getFieldsForUpdate(clientDetails));
140 if (count != 1) {
141 throw new NoSuchClientException("No client found with id = " + clientDetails.getClientId());
142 }
143 }
144
145 public void updateClientSecret(String clientId, String secret) throws NoSuchClientException {
146 int count = jdbcTemplate.update(updateClientSecretSql, passwordEncoder.encode(secret), clientId);
147 if (count != 1) {
148 throw new NoSuchClientException("No client found with id = " + clientId);
149 }
150 }
151
152 public void removeClientDetails(String clientId) throws NoSuchClientException {
153 int count = jdbcTemplate.update(deleteClientDetailsSql, clientId);
154 if (count != 1) {
155 throw new NoSuchClientException("No client found with id = " + clientId);
156 }
157 }
158
159 public List<ClientDetails> listClientDetails() {
160 return listFactory.getList(findClientDetailsSql, Collections.<String, Object> emptyMap(), rowMapper);
161 }
162
163 private Object[] getFields(ClientDetails clientDetails) {
164 Object[] fieldsForUpdate = getFieldsForUpdate(clientDetails);
165 Object[] fields = new Object[fieldsForUpdate.length + 1];
166 System.arraycopy(fieldsForUpdate, 0, fields, 1, fieldsForUpdate.length);
167 fields[0] = clientDetails.getClientSecret() != null ? passwordEncoder.encode(clientDetails.getClientSecret())
168 : null;
169 return fields;
170 }
171
172 private Object[] getFieldsForUpdate(ClientDetails clientDetails) {
173 String json = null;
174 try {
175 json = mapper.write(clientDetails.getAdditionalInformation());
176 }
177 catch (Exception e) {
178 logger.warn("Could not serialize additional information: " + clientDetails, e);
179 }
180 return new Object[] {
181 clientDetails.getResourceIds() != null ? StringUtils.collectionToCommaDelimitedString(clientDetails
182 .getResourceIds()) : null,
183 clientDetails.getScope() != null ? StringUtils.collectionToCommaDelimitedString(clientDetails
184 .getScope()) : null,
185 clientDetails.getAuthorizedGrantTypes() != null ? StringUtils
186 .collectionToCommaDelimitedString(clientDetails.getAuthorizedGrantTypes()) : null,
187 clientDetails.getRegisteredRedirectUri() != null ? StringUtils
188 .collectionToCommaDelimitedString(clientDetails.getRegisteredRedirectUri()) : null,
189 clientDetails.getAuthorities() != null ? StringUtils.collectionToCommaDelimitedString(clientDetails
190 .getAuthorities()) : null, clientDetails.getAccessTokenValiditySeconds(),
191 clientDetails.getRefreshTokenValiditySeconds(), json, getAutoApproveScopes(clientDetails),
192 clientDetails.getClientId() };
193 }
194
195 private String getAutoApproveScopes(ClientDetails clientDetails) {
196 if (clientDetails.isAutoApprove("true")) {
197 return "true";
198 }
199 Set<String> scopes = new HashSet<String>();
200 for (String scope : clientDetails.getScope()) {
201 if (clientDetails.isAutoApprove(scope)) {
202 scopes.add(scope);
203 }
204 }
205 return StringUtils.collectionToCommaDelimitedString(scopes);
206 }
207
208 public void setSelectClientDetailsSql(String selectClientDetailsSql) {
209 this.selectClientDetailsSql = selectClientDetailsSql;
210 }
211
212 public void setDeleteClientDetailsSql(String deleteClientDetailsSql) {
213 this.deleteClientDetailsSql = deleteClientDetailsSql;
214 }
215
216 public void setUpdateClientDetailsSql(String updateClientDetailsSql) {
217 this.updateClientDetailsSql = updateClientDetailsSql;
218 }
219
220 public void setUpdateClientSecretSql(String updateClientSecretSql) {
221 this.updateClientSecretSql = updateClientSecretSql;
222 }
223
224 public void setInsertClientDetailsSql(String insertClientDetailsSql) {
225 this.insertClientDetailsSql = insertClientDetailsSql;
226 }
227
228 public void setFindClientDetailsSql(String findClientDetailsSql) {
229 this.findClientDetailsSql = findClientDetailsSql;
230 }
231
232
233
234
235 public void setListFactory(JdbcListFactory listFactory) {
236 this.listFactory = listFactory;
237 }
238
239
240
241
242 public void setRowMapper(RowMapper<ClientDetails> rowMapper) {
243 this.rowMapper = rowMapper;
244 }
245
246
247
248
249
250
251
252 private static class ClientDetailsRowMapper implements RowMapper<ClientDetails> {
253 private JsonMapper mapper = createJsonMapper();
254
255 public ClientDetails mapRow(ResultSet rs, int rowNum) throws SQLException {
256 BaseClientDetails details = new BaseClientDetails(rs.getString(1), rs.getString(3), rs.getString(4),
257 rs.getString(5), rs.getString(7), rs.getString(6));
258 details.setClientSecret(rs.getString(2));
259 if (rs.getObject(8) != null) {
260 details.setAccessTokenValiditySeconds(rs.getInt(8));
261 }
262 if (rs.getObject(9) != null) {
263 details.setRefreshTokenValiditySeconds(rs.getInt(9));
264 }
265 String json = rs.getString(10);
266 if (json != null) {
267 try {
268 @SuppressWarnings("unchecked")
269 Map<String, Object> additionalInformation = mapper.read(json, Map.class);
270 details.setAdditionalInformation(additionalInformation);
271 }
272 catch (Exception e) {
273 logger.warn("Could not decode JSON for additional information: " + details, e);
274 }
275 }
276 String scopes = rs.getString(11);
277 if (scopes != null) {
278 details.setAutoApproveScopes(StringUtils.commaDelimitedListToSet(scopes));
279 }
280 return details;
281 }
282 }
283
284 interface JsonMapper {
285 String write(Object input) throws Exception;
286
287 <T> T read(String input, Class<T> type) throws Exception;
288 }
289
290 private static JsonMapper createJsonMapper() {
291 if (ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", null)) {
292 return new Jackson2Mapper();
293 }
294 return new NotSupportedJsonMapper();
295 }
296
297 private static class Jackson2Mapper implements JsonMapper {
298 private com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
299
300 @Override
301 public String write(Object input) throws Exception {
302 return mapper.writeValueAsString(input);
303 }
304
305 @Override
306 public <T> T read(String input, Class<T> type) throws Exception {
307 return mapper.readValue(input, type);
308 }
309 }
310
311 private static class NotSupportedJsonMapper implements JsonMapper {
312 @Override
313 public String write(Object input) throws Exception {
314 throw new UnsupportedOperationException(
315 "Jackson 2 is not available so JSON conversion cannot be done");
316 }
317
318 @Override
319 public <T> T read(String input, Class<T> type) throws Exception {
320 throw new UnsupportedOperationException(
321 "Jackson 2 is not available so JSON conversion cannot be done");
322 }
323 }
324
325 }