Java tutorial
/** * Copyright 2018, Radiant Solutions, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. **/ package org.venice.beachfront.bfapi; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.regex.Pattern; import javax.net.ssl.SSLContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.http.HeaderElement; import org.apache.http.HeaderElementIterator; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.ProtocolException; import org.apache.http.client.HttpClient; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpHead; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.methods.RequestBuilder; import org.apache.http.config.Registry; import org.apache.http.config.RegistryBuilder; import org.apache.http.conn.ConnectionKeepAliveStrategy; import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.conn.ssl.TrustSelfSignedStrategy; import org.apache.http.cookie.Cookie; import org.apache.http.cookie.CookieOrigin; import org.apache.http.cookie.CookieSpec; import org.apache.http.cookie.CookieSpecProvider; import org.apache.http.cookie.MalformedCookieException; import org.apache.http.impl.client.BasicCookieStore; import org.apache.http.impl.client.DefaultRedirectStrategy; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.cookie.DefaultCookieSpec; import org.apache.http.message.BasicHeaderElementIterator; import org.apache.http.protocol.HTTP; import org.apache.http.protocol.HttpContext; import org.apache.http.ssl.SSLContexts; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.http.converter.FormHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.client.RestTemplate; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import org.venice.beachfront.bfapi.auth.ApiKeyAuthProvider; import org.venice.beachfront.bfapi.auth.ExtendedRequestDetails; import org.venice.beachfront.bfapi.auth.FailedAuthEntryPoint; import org.venice.beachfront.bfapi.auth.jwt.JWTAuthProvider; import org.venice.beachfront.bfapi.auth.jwt.JWTAuthenticationFilter; import org.venice.beachfront.bfapi.geoserver.AuthHeaders; import org.venice.beachfront.bfapi.geoserver.BasicAuthHeaders; import org.venice.beachfront.bfapi.geoserver.PKIAuthHeaders; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.joda.JodaModule; import io.swagger.annotations.ApiOperation; import model.logger.Severity; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.service.Contact; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import util.GeoAxisJWTUtility; import util.PiazzaLogger; @Configuration public class BfApiConfig { @Value("${http.keep.alive.duration.seconds}") private int httpKeepAliveDurationSeconds; @Value("${GEOAXIS_JWT_CERT:}") private String geoaxisJwtCertPath; @Value("${jwt.enabled}") private Boolean enableJwt; @Bean(name = "rest-template-no-follow-redirect") public RestTemplate getHttpClientWithoutRedirects() { return new RestTemplate(new HttpComponentsClientHttpRequestFactory( HttpClientBuilder.create().disableRedirectHandling().build())); } @Bean public ObjectMapper getJacksonObjectMapper() { ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(new JodaModule()); return mapper; } @Bean public ExecutorService getExecutor(@Value("${concurrent.threads}") int threads) { return Executors.newFixedThreadPool(threads); } @Bean public GeoAxisJWTUtility getGeoAxisJWTUtility() { if (enableJwt.booleanValue()) { // Load the cert file try (final InputStream inputStream = getClass().getClassLoader() .getResourceAsStream(geoaxisJwtCertPath)) { return new GeoAxisJWTUtility(inputStream); } catch (CertificateException | IOException exception) { throw new BeanCreationException(String.format( "Could not create JWT certificate utility bean. Error loading JWT certificate: %s", exception.getMessage())); } } return new GeoAxisJWTUtility(); // Mocking } /** * Ensures proper CORS headers are present in all requests */ @Configuration protected static class AddCorsHeaders extends WebMvcConfigurerAdapter { @Value("${DOMAIN}") private String domain; @Value("${auth.allowedOrigins}") private String allowedOrigins; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new HandlerInterceptorAdapter() { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { final String origin = request.getHeader("Origin"); final String sanitizedOrigin = removeSpecialCharacters(origin); final List<String> allowedOriginsList = Arrays.asList(allowedOrigins.split(",")); final boolean isAllowed = allowedOriginsList.stream() .anyMatch(str -> str.trim().equals(sanitizedOrigin)); if (isAllowed) { response.setHeader("Access-Control-Allow-Origin", sanitizedOrigin); } response.setHeader("Access-Control-Allow-Headers", "authorization, content-type, X-Requested-With"); response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader("Access-Control-Max-Age", "36000"); return true; } }); } private static String removeSpecialCharacters(String input) { return (input != null) ? input.replaceAll("\\r", "").replaceAll("\\n", "") : null; } } /** * Ensures proper CORS headers are present in all requests */ @Configuration protected static class AddCrsfFilter extends WebMvcConfigurerAdapter { @Value("${auth.allowedOrigins}") private String allowedOrigins; @Value("${auth.publicEndpoints}") private String publicEndpoints; @Autowired private PiazzaLogger piazzaLogger; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new HandlerInterceptorAdapter() { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // Allow OPTIONS for these purposes if ("OPTIONS".equals(request.getMethod())) { return true; } // Allow public endpoints if (isPublicEndpoint(request.getServletPath())) { return true; } final String origin = request.getHeader(HttpHeaders.ORIGIN); final String referer = request.getHeader(HttpHeaders.REFERER); final String requestedWith = request.getHeader("X-Requested-With"); final String allowedRequestHeaders = request .getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS); final boolean isAccessControlRequestHeader = (allowedRequestHeaders != null) ? allowedRequestHeaders.contains("X-Requested-With") : false; final boolean isRequestXhr = "XMLHttpRequest".equals(requestedWith); final boolean isXhr = isRequestXhr || isAccessControlRequestHeader; if (isAllowedOrigin(origin) && isXhr) { // Allow cors request from approved endpoint return true; } if ((origin == null || origin.isEmpty()) && (referer == null || referer.isEmpty())) { // Allow non-CORS request return true; } piazzaLogger.log(String.format( "Possible CSRF attempt: endpoint=`%s` origin=`%s` referrer=`%s` ip=`%s` is_xhr=`%s`", request.getServletPath(), origin, referer, request.getRemoteAddr(), isXhr), Severity.WARNING); response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied: CORS request validation failed"); return false; } }); } private boolean isAllowedOrigin(String origin) { final List<String> allowedOriginsList = Arrays.asList(allowedOrigins.split(",")); return allowedOriginsList.stream().anyMatch(str -> str.trim().equals(origin)); } private boolean isPublicEndpoint(String path) { final List<String> pubicEndpointsList = Arrays.asList(publicEndpoints.split(",")); return pubicEndpointsList.stream().anyMatch(str -> Pattern.compile(str).matcher(path).matches()); } } @Configuration @Profile({ "insecure" }) public class DisabledApplicationSecurity extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers("/**").permitAll(); http.csrf().disable(); } } /** * Initializes the security providers for validating API Keys on all requests that require API Key validation. */ @Configuration @Profile({ "cloud" }) protected static class ApplicationSecurity extends WebSecurityConfigurerAdapter { @Autowired private ApiKeyAuthProvider apiKeyAuthProvider; @Autowired private JWTAuthProvider jwtAuthProvider; @Autowired private FailedAuthEntryPoint failureEntryPoint; @Value("${jwt.enabled}") private Boolean enableJwt; @Override public void configure(HttpSecurity http) throws Exception { HttpSecurity security = http.authorizeRequests().antMatchers(HttpMethod.OPTIONS).permitAll() // Allow any OPTIONS for REST best // practices .antMatchers("/").permitAll() // Allow unauthenticated queries to root (health check) path .antMatchers("/oauth/callback").permitAll() // Allow unauthenticated queries to login callback path .antMatchers("/oauth/start").permitAll() // Allow unauthenticated queries to OAuth login start path .antMatchers("/v2/api-docs").permitAll() // Allow unauthenticated queries for Swagger documentation .antMatchers("/ia/**").permitAll() // Allow unauthenticated IA Broker requests, necessary for map tiles .anyRequest().authenticated() // All other requests must be authenticated .and().httpBasic() // Use HTTP Basic authentication .authenticationEntryPoint(this.failureEntryPoint) // Entry point for starting a Basic auth exchange // (i.e. "failed authentication" handling) .authenticationDetailsSource(this.authenticationDetailsSource()) // Feed more request details into // any providers .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // Do not create // or manage // sessions for // security .and().logout().disable() // Disable auto-magical Spring Security logout behavior .authenticationProvider(this.apiKeyAuthProvider) // Use this custom authentication provider to // authenticate requests .csrf().disable(); // Disable advanced CSRF protections for better statelessness // Add JWT authentication filters if enabled if (enableJwt.booleanValue()) { final JWTAuthenticationFilter jwtFilter = new JWTAuthenticationFilter( this.authenticationManagerBean(), failureEntryPoint); security.addFilterAfter(jwtFilter, UsernamePasswordAuthenticationFilter.class); // Filtering Bearer Token Auth security.authenticationProvider(this.jwtAuthProvider); // Authenticating JWT Tokens } } private AuthenticationDetailsSource<HttpServletRequest, ExtendedRequestDetails> authenticationDetailsSource() { return new AuthenticationDetailsSource<HttpServletRequest, ExtendedRequestDetails>() { @Override public ExtendedRequestDetails buildDetails(HttpServletRequest request) { return new ExtendedRequestDetails(request); } }; } } /** * Configures the API to use basic HTTP authentication when communicating with GeoServer */ @Profile({ "basic-geoserver-auth" }) protected class BasicAuthenticationConfig { @Value("${http.max.total}") private int httpMaxTotal; @Value("${http.max.route}") private int httpMaxRoute; @Bean public HttpClient httpClient() { return HttpClientBuilder.create().setMaxConnTotal(httpMaxTotal).setMaxConnPerRoute(httpMaxRoute) .setSSLHostnameVerifier(new NoopHostnameVerifier()).setKeepAliveStrategy(getKeepAliveStrategy()) .build(); } @Bean public RestTemplate restTemplate(@Autowired HttpClient httpClient) { final RestTemplate restTemplate = new RestTemplate(); restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory(httpClient)); restTemplate.getMessageConverters().add(new FormHttpMessageConverter()); return restTemplate; } @Bean public AuthHeaders authHeaders() { return new BasicAuthHeaders(); } } /** * Configures the API to use PKI authentication when communicating with GeoServer */ @Configuration @Profile({ "pki-geoserver-auth" }) protected class PKIAuthenticationConfig { @Value("${http.max.total}") private int httpMaxTotal; @Value("${http.max.route}") private int httpMaxRoute; @Value("${JKS_FILE}") private String keystoreFileName; @Value("${JKS_PASSPHRASE}") private String keystorePassphrase; @Value("${PZ_PASSPHRASE}") private String piazzaKeyPassphrase; @Bean public HttpClient httpClient() throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, UnrecoverableKeyException, KeyManagementException { final SSLContext sslContext = SSLContexts.custom() .loadKeyMaterial(getStore(), piazzaKeyPassphrase.toCharArray()) .loadTrustMaterial(null, new TrustSelfSignedStrategy()).useProtocol("TLS").build(); final Registry<CookieSpecProvider> registry = RegistryBuilder.<CookieSpecProvider>create() .register("myspec", new MySpecProvider()).build(); final RequestConfig requestConfig = RequestConfig.custom().setCookieSpec("myspec") .setCircularRedirectsAllowed(true).build(); return HttpClientBuilder.create().setDefaultRequestConfig(requestConfig).setMaxConnTotal(httpMaxTotal) .setSSLContext(sslContext).setSSLHostnameVerifier(new NoopHostnameVerifier()) .setDefaultCookieStore(new BasicCookieStore()).setDefaultCookieSpecRegistry(registry) .setRedirectStrategy(new MyRedirectStrategy()).setMaxConnPerRoute(httpMaxRoute) .setKeepAliveStrategy(getKeepAliveStrategy()).build(); } @Bean public RestTemplate restTemplate(@Autowired HttpClient httpClient) { final RestTemplate restTemplate = new RestTemplate(); restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory(httpClient)); final List<HttpMessageConverter<?>> messageConverters = new ArrayList<>(); messageConverters.add(new StringHttpMessageConverter()); messageConverters.add(new MappingJackson2HttpMessageConverter()); messageConverters.add(new FormHttpMessageConverter()); restTemplate.setMessageConverters(messageConverters); return restTemplate; } @Bean public AuthHeaders authHeaders() { return new PKIAuthHeaders(); } protected KeyStore getStore() throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException { final KeyStore store = KeyStore.getInstance(KeyStore.getDefaultType()); try (final InputStream inputStream = getClass().getClassLoader() .getResourceAsStream(keystoreFileName)) { store.load(inputStream, keystorePassphrase.toCharArray()); } return store; } protected class MyCookieSpec extends DefaultCookieSpec { @Override public void validate(Cookie c, CookieOrigin co) throws MalformedCookieException { // Do nothing; accept all cookies } } protected class MySpecProvider implements CookieSpecProvider { @Override public CookieSpec create(HttpContext context) { return new MyCookieSpec(); } } protected class MyRedirectStrategy extends DefaultRedirectStrategy { @Override protected boolean isRedirectable(final String method) { return true; } @Override public HttpUriRequest getRedirect(final HttpRequest request, final HttpResponse response, final HttpContext context) throws ProtocolException { final URI uri = getLocationURI(request, response, context); final String method = request.getRequestLine().getMethod(); if (method.equalsIgnoreCase(HttpHead.METHOD_NAME)) { return new HttpHead(uri); } else if (method.equalsIgnoreCase(HttpGet.METHOD_NAME)) { return new HttpGet(uri); } else { final int status = response.getStatusLine().getStatusCode(); if (status == HttpStatus.SC_TEMPORARY_REDIRECT || status == HttpStatus.SC_MOVED_PERMANENTLY || status == HttpStatus.SC_MOVED_TEMPORARILY) { return RequestBuilder.copy(request).setUri(uri).build(); } else { return new HttpGet(uri); } } } } } /** * Defines a keep-alive strategy for requests that do not provide a proper timeout value. This will avoid a case * where keep-alive is set to permanent (when the header is missing) in cases where this application's environment * will not allow for permanent connections. */ private ConnectionKeepAliveStrategy getKeepAliveStrategy() { // Returns the keep alive duration, in milliseconds return (HttpResponse response, HttpContext context) -> { HeaderElementIterator it = new BasicHeaderElementIterator( response.headerIterator(HTTP.CONN_KEEP_ALIVE)); while (it.hasNext()) { final HeaderElement headerElement = it.nextElement(); final String param = headerElement.getName(); final String value = headerElement.getValue(); if (value != null && param.equalsIgnoreCase("timeout")) { return Long.parseLong(value) * 1000; } } return httpKeepAliveDurationSeconds * 1000; }; } @Bean public Docket beachfrontApi() { ApiInfo apiInfo = new ApiInfoBuilder().title("Beachfront API").description("Beachfront Web Services") .contact(new Contact("The VeniceGeo Project", "http://radiantblue.com", "venice@radiantblue.com")) .version("0.1.0").build(); return new Docket(DocumentationType.SWAGGER_2).useDefaultResponseMessages(false) .ignoredParameterTypes(Authentication.class).groupName("Beachfront").apiInfo(apiInfo).select() .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class)).paths(PathSelectors.any()) .build(); } }