001/* 002 * Copyright (c) 2013 - 2016 TDP Ltd All Rights Reserved. 003 * TDP Ltd grants permission, free of charge, to any person obtaining copies 004 * of this software and its associated documentation files (the "Software"), 005 * to deal in the Software without restriction, including to use, copy, adapt, 006 * publish, distribute, display, perform, sublicense, and sell copies of the 007 * Software, subject to the following condition: You must include the above 008 * copyright notice and this permission notice in all full or partial copies 009 * of the Software. 010 * 011 * TDP LTD PROVIDES THE SOFTWARE "AS IS," WITHOUT ANY EXPRESS OR IMPLIED WARRANTY, 012 * INCLUDING WITHOUT THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 013 * PARTICULAR PURPOSE, AND NON-INFRINGMENT. TDP LTD, THE AUTHORS OF THE SOFTWARE, 014 * AND THE OWNERS OF COPYRIGHT IN THE SOFTWARE ARE NOT LIABLE FOR ANY CLAIM, DAMAGES, 015 * OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING 016 * FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 017 * THE SOFTWARE. 018 */ 019package cz.tdp.kshield.integration; 020 021import java.io.FileInputStream; 022import java.io.IOException; 023import java.io.InputStream; 024import java.security.KeyManagementException; 025import java.security.KeyStore; 026import java.security.KeyStoreException; 027import java.security.NoSuchAlgorithmException; 028import java.security.cert.CertificateException; 029import java.util.EnumSet; 030 031import javax.annotation.PostConstruct; 032import javax.annotation.PreDestroy; 033import javax.net.ssl.SSLContext; 034 035import org.apache.commons.logging.Log; 036import org.apache.commons.logging.LogFactory; 037import org.apache.http.client.config.RequestConfig; 038import org.apache.http.config.Registry; 039import org.apache.http.config.RegistryBuilder; 040import org.apache.http.config.SocketConfig; 041import org.apache.http.conn.HttpClientConnectionManager; 042import org.apache.http.conn.socket.ConnectionSocketFactory; 043import org.apache.http.conn.socket.PlainConnectionSocketFactory; 044import org.apache.http.conn.ssl.SSLConnectionSocketFactory; 045import org.apache.http.conn.ssl.SSLContexts; 046import org.apache.http.impl.client.CloseableHttpClient; 047import org.apache.http.impl.client.HttpClients; 048import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; 049 050import cz.tdp.kshield.client.ClientMessage; 051import cz.tdp.kshield.client.KShieldClient; 052import cz.tdp.kshield.client.KShieldClientException; 053import cz.tdp.kshield.client.ResponseLevel; 054import cz.tdp.kshield.client.UserInfo; 055import cz.tdp.kshield.client.UserInfo.AuthType; 056 057/** 058 * Basic implementation of KeyShield SSO Server AuthenticationService. 059 * KShieldClient is used internally to communicate with KeyShield SSO Server. 060 * 061 * @see cz.tdp.kshield.client.KShieldClient 062 */ 063public class SimpleAuthenticationServiceImpl implements AuthenticationService 064{ 065 protected KShieldClient client; 066 067 protected HttpClientConnectionManager connManager; 068 protected CloseableHttpClient httpClient; 069 070 /** 071 * @param url KeyShield SSO Server url 072 */ 073 public SimpleAuthenticationServiceImpl(String url) { 074 this.url = url; 075 } 076 077 protected void checkUrl() { 078 if (url == null || url.length() == 0) throw new IllegalArgumentException("Please provide valid KeyShield SSO server URL"); 079 } 080 081 /** 082 * Initializes Authentication service after creation (In Spring v 3.x and Guice IOC called automatically) 083 * 084 * <p><em>Important - call this method after creation and overall setup of AuthenticationService instance 085 */ 086 @Override 087 @PostConstruct 088 public void init() { 089 log.info("Starting KeyShield SSO AuthenticationService"); 090 091 checkUrl(); 092 093 if (httpClient == null) { 094 log.info("Creating HttpClientConnectionManager"); 095 Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create() 096 .register("http", PlainConnectionSocketFactory.getSocketFactory()) 097 .register("https", createSSLSocketFactory()) 098 .build(); 099 100 connManager = new PoolingHttpClientConnectionManager(registry); 101 102 log.info("Creating HttpClient"); 103 SocketConfig socketConfig = SocketConfig.custom() 104 .setTcpNoDelay(true) 105 .build(); 106 107 RequestConfig requestConfig = RequestConfig.custom() 108 .setSocketTimeout(soTimeout) 109 .setConnectTimeout(connectionTimeout) 110 .build(); 111 112 httpClient = HttpClients.custom() 113 .setConnectionManager(connManager) 114 .setDefaultSocketConfig(socketConfig) 115 .setDefaultRequestConfig(requestConfig) 116 .build(); 117 } 118 119 log.info("Create KShieldClient: kshieldUrl=" + url + ", apiKey=" + apiKey); 120 121 if (hasOptionalAttributes()) { 122 client = new KShieldClient(url, apiKey, httpClient, getMergedAttributes()); 123 } 124 else { 125 client = new KShieldClient(url, apiKey, httpClient); 126 } 127 128 log.info("Setting Allowed Auth types to: " + allowedAuthTypes); 129 client.setAllowedAuthTypes(allowedAuthTypes); 130 131 log.info("Setting ResponseLevel to: " + responseLevel); 132 client.setResponseLevel(responseLevel); 133 } 134 135 /** 136 * Cleanup Authentication service before destruction (In Spring v 3.x and Guice IOC called automatically) 137 */ 138 @Override 139 @PreDestroy 140 public void destroy() { 141 log.info("Clean up KeyShield SSO AuthenticationService"); 142 143 if (httpClient != null) { 144 try { 145 httpClient.close(); 146 } 147 catch (IOException e) { 148 log.warn("error while closing httpClient", e); 149 } 150 151 httpClient = null; 152 } 153 154 if (connManager != null) { 155 connManager.shutdown(); 156 connManager = null; 157 } 158 159 client = null; 160 161 log.info("Clean up finished"); 162 } 163 164 @Override 165 public void checkService() throws KShieldClientException { 166 if (client == null) throw new IllegalStateException("This AuthenticaticationService must be initialized (method init) before attempt to authenticate"); 167 168 client.getUserByIP(TEST_IP_ADDR); 169 } 170 171 /** 172 * @param ipAddr remote request IP address 173 * @return new instance of <code>UserInfo</code> if successfully authenticated or null otherwise 174 */ 175 @Override 176 public UserInfo createUserInfo(String ipAddr) { 177 if (client == null) throw new IllegalStateException("This AuthenticaticationService must be initialized (method init) before attempt to authenticate"); 178 179 if (ipAddr == null || ipAddr.length() == 0) { 180 //XXX maybe throw IllegalArgumentException? 181 return null; 182 } 183 184 try { 185 KShieldContext.initKShieldSession(); 186 187 final UserInfo userInfo = client.getUserByIP(ipAddr, usernameAttribute); 188 189 if (userInfo != null && userInfoValidator.validate(userInfo)) { 190 // kshield session is closed by caller of this method 191 192 KShieldContext.startKShieldSession(); 193 return userInfo; 194 } 195 } 196 catch (KShieldClientException e) { 197 log.error("client error", e); 198 } 199 200 KShieldContext.closeKShieldSession(); 201 return null; 202 } 203 204 @Override 205 public void sendClientMessage(String from, String to, String message) throws KShieldClientException { 206 sendClientMessage(new ClientMessage(from, to, message)); 207 } 208 209 @Override 210 public void sendClientMessage(ClientMessage msg) throws KShieldClientException { 211 if (client == null) throw new IllegalStateException("This AuthenticaticationService must be initialized (method init) before attempt to authenticate"); 212 213 try { 214 client.sendClientMessage(msg); 215 } 216 catch (KShieldClientException e) { 217 log.error("client error", e); 218 throw e; 219 } 220 } 221 222 protected String url; 223 224 /** 225 * @return KeyShield SSO Server url 226 */ 227 public String getUrl() { 228 return this.url; 229 } 230 231 /** 232 * Sets KeyShield SSO Server url 233 * <p><em>Important - set this before init() method call 234 */ 235 public void setUrl(String url) { 236 this.url = url; 237 } 238 239 private String apiKey; 240 241 /** 242 * @return KeyShield SSO API authorization key 243 */ 244 public String getApiKey() { 245 return this.apiKey; 246 } 247 248 /** 249 * Sets KeyShield SSO API authorization key 250 * <p><em>Important - set this before init() method call 251 * @param apiKey 252 */ 253 public void setApiKey(String apiKey) { 254 this.apiKey = apiKey; 255 } 256 257 private String usernameAttribute; 258 259 /** 260 * @return UserInfo attribute used as username 261 */ 262 public String getUsernameAttribute() { 263 return usernameAttribute; 264 } 265 266 /** 267 * Set name of attribute used as username instead of screenName 268 * This attribute is automatically merged with optional attributes 269 * <p><em>Important - set this before init() method call 270 * 271 * @param usernameAttr name of username attribute 272 */ 273 public void setUsernameAttribute(String usernameAttr) { 274 this.usernameAttribute = usernameAttr; 275 } 276 277 private static final String[] EMPTY_ATTRS = new String[0]; 278 279 private String[] optionalAttributes = EMPTY_ATTRS; 280 281 /** 282 * Set optional attributes requested from KeyShield SSO with UserInfo 283 * <p><em>Important - set this before init() method call 284 * 285 * @param attrs optional attributes names 286 */ 287 public void setOptionalAttributes(String... attrs) { 288 if (attrs != null && attrs.length > 0) { 289 this.optionalAttributes = attrs; 290 } 291 else { 292 this.optionalAttributes = EMPTY_ATTRS; 293 } 294 } 295 296 protected String[] getMergedAttributes() { 297 final boolean hasUserAttr = hasUsernameAttr(); 298 final int optAttrSize = optionalAttributes.length; 299 300 if (hasUserAttr || optAttrSize > 0) { 301 final String[] merged = new String[optAttrSize + (hasUserAttr ? 1 : 0)]; 302 303 if (hasUserAttr) { 304 merged[0] = usernameAttribute; 305 } 306 if (optAttrSize > 0) { 307 final int optAttrIndex = hasUserAttr ? 1 : 0; 308 System.arraycopy(optionalAttributes, 0, merged, optAttrIndex, optAttrSize); 309 } 310 311 return merged; 312 } 313 314 return null; 315 } 316 317 private EnumSet<AuthType> allowedAuthTypes = UserInfo.DEFAULT_ALLOWED_AUTH_TYPES; 318 319 /** 320 * @returns allowed authentication types 321 * @see AuthType 322 */ 323 public EnumSet<AuthType> getAllowedAuthTypes() { 324 return allowedAuthTypes; 325 } 326 327 /** 328 * Sets allowed authentication types 329 * <p><em>Important - set this before init() method call 330 * 331 * @param allowAuthTypes set of allowed authentication types 332 * @see AuthType 333 */ 334 public void setAllowedAuthTypes(EnumSet<AuthType> allowAuthTypes) { 335 this.allowedAuthTypes = allowAuthTypes; 336 } 337 338 private ResponseLevel responseLevel; 339 340 /** 341 * @return configured client response level 342 * @see ResponseLevel 343 */ 344 public ResponseLevel getResponseLevel() { 345 return this.responseLevel; 346 } 347 348 /** 349 * Sets optional response level used in KeyShieldSSO requests 350 * It is possible to set this dynamically after init() method 351 * 352 * @param responseLevel 353 */ 354 public void setResponseLevel(ResponseLevel responseLevel) { 355 this.responseLevel = responseLevel; 356 357 if (client != null) { 358 client.setResponseLevel(responseLevel); 359 } 360 } 361 362 /** 363 * Connection timeout 364 */ 365 private int connectionTimeout = 5000; 366 367 /** 368 * Returns http connection timeout in milliseconds - default is 5000 369 */ 370 public int getConnectionTimeout() { 371 return connectionTimeout; 372 } 373 374 /** 375 * Sets http connection timeout in milliseconds - default is 5000 376 * <p><em>Important - set this before init() method call 377 * 378 * @param connectionTimeout 379 */ 380 public void setConnectionTimeout(int connectionTimeout) { 381 this.connectionTimeout = connectionTimeout; 382 } 383 384 /** 385 * Socket timeout 386 */ 387 private int soTimeout = 5000; 388 389 /** 390 * Return SO_TIMEOUT in milliseconds - default is 5000 391 */ 392 public int getSoTimeout() { 393 return soTimeout; 394 } 395 396 /** 397 * Set SO_TIMEOUT in milliseconds - default is 5000 398 * <p><em>Important - set this before init() method call 399 * 400 * @param soTimeout 401 */ 402 public void setSoTimeout(int soTimeout) { 403 this.soTimeout = soTimeout; 404 } 405 406 private UserInfoValidator userInfoValidator = new SimpleUserInfoValidator(); 407 408 /** 409 * Return custom userInfo validator 410 */ 411 public UserInfoValidator getUserInfoValidator() { 412 return this.userInfoValidator; 413 } 414 415 /** 416 * Set custom userInfo validator 417 */ 418 public void setUserInfoValidator(UserInfoValidator userInfoValidator) { 419 if (userInfoValidator != null) { 420 this.userInfoValidator = userInfoValidator; 421 } 422 else { 423 this.userInfoValidator = new SimpleUserInfoValidator(); 424 } 425 } 426 427 private String trustStorePath; 428 429 /** 430 * @return Custom JKS truststore path 431 */ 432 public String getTrustStorePath() { 433 return this.trustStorePath; 434 } 435 436 /** 437 * Sets custom JKS truststore path 438 * This truststore will be used as in memory keystore - all certificates are treated as trusted 439 * <p><em>Important - set this before init() method call 440 * 441 * @param trustStorePath 442 */ 443 public void setTrustStorePath(String trustStorePath) { 444 this.trustStorePath = trustStorePath; 445 } 446 447 protected boolean hasOptionalAttributes() { 448 return hasUsernameAttr() || optionalAttributes.length > 0; 449 } 450 451 protected boolean hasUsernameAttr() { 452 return usernameAttribute != null && !usernameAttribute.isEmpty(); 453 } 454 455 protected boolean hasTrustStore() { 456 return trustStorePath != null && !trustStorePath.isEmpty(); 457 } 458 459 protected SSLConnectionSocketFactory createSSLSocketFactory() { 460 try { 461 if (hasTrustStore()) { 462 log.info("Loading Truststore from: " + trustStorePath); 463 464 final KeyStore truststore = loadTrustStore(); 465 466 if (truststore != null) { 467 log.info("Create InMemoryTrustStoreSSLFactory"); 468 469 final SSLContext sslContext = SSLContexts.custom() 470 .loadTrustMaterial(truststore) 471 .build(); 472 473 return new SSLConnectionSocketFactory(sslContext); 474 } 475 } 476 } 477 catch (KeyManagementException|NoSuchAlgorithmException|KeyStoreException e) { 478 log.error("failed create SSL Socket Factory", e); 479 } 480 481 return SSLConnectionSocketFactory.getSocketFactory(); 482 } 483 484 protected KeyStore loadTrustStore() { 485 try (InputStream in = new FileInputStream(trustStorePath)) { 486 KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); 487 trustStore.load(in, null); 488 return trustStore; 489 } 490 catch (NoSuchAlgorithmException|CertificateException|KeyStoreException|IOException e) { 491 log.error("Cannot load Trust Store", e); 492 } 493 494 log.warn("Unable to load truststore from path: " + trustStorePath); 495 return null; 496 } 497 498 //NOSONAR-BEGIN hard coded IP 499 private static final String TEST_IP_ADDR = "0.0.0.0"; 500 //NOSONAR-END 501 502 private static final Log log = LogFactory.getLog(SimpleAuthenticationServiceImpl.class); 503}