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}