View Javadoc
1   /*
2    * Copyright 2008 Web Cohesion
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  
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   * Basic, JDBC implementation of the client details service.
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 	 * @param passwordEncoder the password encoder to set
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"; // all scopes autoapproved
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 	 * @param listFactory the list factory to set
234 	 */
235 	public void setListFactory(JdbcListFactory listFactory) {
236 		this.listFactory = listFactory;
237 	}
238 
239 	/**
240 	 * @param rowMapper the rowMapper to set
241 	 */
242 	public void setRowMapper(RowMapper<ClientDetails> rowMapper) {
243 		this.rowMapper = rowMapper;
244 	}
245 
246 	/**
247 	 * Row mapper for ClientDetails.
248 	 * 
249 	 * @author Dave Syer
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 }