/* * Copyright 2015 the original author or authors. * * 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.springframework.security.kerberos.client; import java.net.URI; import java.security.Principal; import java.security.PrivilegedAction; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import javax.security.auth.Subject; import javax.security.auth.kerberos.KerberosPrincipal; import javax.security.auth.login.AppConfigurationEntry; import javax.security.auth.login.Configuration; import javax.security.auth.login.LoginContext; import org.apache.http.auth.AuthSchemeProvider; import org.apache.http.auth.AuthScope; import org.apache.http.auth.Credentials; import org.apache.http.client.HttpClient; import org.apache.http.client.config.AuthSchemes; import org.apache.http.config.Lookup; import org.apache.http.config.RegistryBuilder; import org.apache.http.impl.auth.SPNegoSchemeFactory; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.springframework.http.HttpMethod; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.util.StringUtils; import org.springframework.web.client.RequestCallback; import org.springframework.web.client.ResponseExtractor; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; /** * {@code RestTemplate} that is able to make kerberos SPNEGO authenticated REST * requests. Under a hood this {@code KerberosRestTemplate} is using {@link HttpClient} to * support Kerberos. * * <p>Generally this template can be configured in few different ways. * <ul> * <li>Leave keyTabLocation and userPrincipal empty if you want to use cached ticket</li> * <li>Use keyTabLocation and userPrincipal if you want to use keytab file</li> * <li>Use loginOptions if you want to customise Krb5LoginModule options</li> * <li>Use a customised httpClient</li> * </ul> * * @author Janne Valkealahti * */ public class KerberosRestTemplate extends RestTemplate { private static final Credentials credentials = new NullCredentials(); private final String keyTabLocation; private final String userPrincipal; private final Map<String, Object> loginOptions; /** * Instantiates a new kerberos rest template. */ public KerberosRestTemplate() { this(null, null, null, buildHttpClient()); } /** * Instantiates a new kerberos rest template. * * @param httpClient the http client */ public KerberosRestTemplate(HttpClient httpClient) { this(null, null, null, httpClient); } /** * Instantiates a new kerberos rest template. * * @param keyTabLocation the key tab location * @param userPrincipal the user principal */ public KerberosRestTemplate(String keyTabLocation, String userPrincipal) { this(keyTabLocation, userPrincipal, buildHttpClient()); } /** * Instantiates a new kerberos rest template. * * @param keyTabLocation the key tab location * @param userPrincipal the user principal * @param httpClient the http client */ public KerberosRestTemplate(String keyTabLocation, String userPrincipal, HttpClient httpClient) { this(keyTabLocation, userPrincipal, null, httpClient); } /** * Instantiates a new kerberos rest template. * * @param loginOptions the login options */ public KerberosRestTemplate(Map<String, Object> loginOptions) { this(null, null, loginOptions, buildHttpClient()); } /** * Instantiates a new kerberos rest template. * * @param loginOptions the login options * @param httpClient the http client */ public KerberosRestTemplate(Map<String, Object> loginOptions, HttpClient httpClient) { this(null, null, loginOptions, httpClient); } /** * Instantiates a new kerberos rest template. * * @param keyTabLocation the key tab location * @param userPrincipal the user principal * @param loginOptions the login options */ public KerberosRestTemplate(String keyTabLocation, String userPrincipal, Map<String, Object> loginOptions) { this(keyTabLocation, userPrincipal, loginOptions, buildHttpClient()); } /** * Instantiates a new kerberos rest template. * * @param keyTabLocation the key tab location * @param userPrincipal the user principal * @param loginOptions the login options * @param httpClient the http client */ private KerberosRestTemplate(String keyTabLocation, String userPrincipal, Map<String, Object> loginOptions, HttpClient httpClient) { super(new HttpComponentsClientHttpRequestFactory(httpClient)); this.keyTabLocation = keyTabLocation; this.userPrincipal = userPrincipal; this.loginOptions = loginOptions; } /** * Builds the default instance of {@link HttpClient} having kerberos * support. * * @return the http client with spneno auth scheme */ private static HttpClient buildHttpClient() { HttpClientBuilder builder = HttpClientBuilder.create(); Lookup<AuthSchemeProvider> authSchemeRegistry = RegistryBuilder.<AuthSchemeProvider> create() .register(AuthSchemes.SPNEGO, new SPNegoSchemeFactory(true)).build(); builder.setDefaultAuthSchemeRegistry(authSchemeRegistry); BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); credentialsProvider.setCredentials(new AuthScope(null, -1, null), credentials); builder.setDefaultCredentialsProvider(credentialsProvider); CloseableHttpClient httpClient = builder.build(); return httpClient; } @Override protected final <T> T doExecute(final URI url, final HttpMethod method, final RequestCallback requestCallback, final ResponseExtractor<T> responseExtractor) throws RestClientException { try { ClientLoginConfig loginConfig = new ClientLoginConfig(keyTabLocation, userPrincipal, loginOptions); Set<Principal> princ = new HashSet<Principal>(1); princ.add(new KerberosPrincipal(userPrincipal)); Subject sub = new Subject(false, princ, new HashSet<Object>(), new HashSet<Object>()); LoginContext lc = new LoginContext("", sub, null, loginConfig); lc.login(); Subject serviceSubject = lc.getSubject(); return Subject.doAs(serviceSubject, new PrivilegedAction<T>() { @Override public T run() { return KerberosRestTemplate.this.doExecuteSubject(url, method, requestCallback, responseExtractor); } }); } catch (Exception e) { throw new RestClientException("Error running rest call", e); } } private <T> T doExecuteSubject(URI url, HttpMethod method, RequestCallback requestCallback, ResponseExtractor<T> responseExtractor) throws RestClientException { return super.doExecute(url, method, requestCallback, responseExtractor); } private static class ClientLoginConfig extends Configuration { private final String keyTabLocation; private final String userPrincipal; private final Map<String, Object> loginOptions; public ClientLoginConfig(String keyTabLocation, String userPrincipal, Map<String, Object> loginOptions) { super(); this.keyTabLocation = keyTabLocation; this.userPrincipal = userPrincipal; this.loginOptions = loginOptions; } @Override public AppConfigurationEntry[] getAppConfigurationEntry(String name) { Map<String, Object> options = new HashMap<String, Object>(); // if we don't have keytab or principal only option is to rely on // credentials cache. if (!StringUtils.hasText(keyTabLocation) || !StringUtils.hasText(userPrincipal)) { // cache options.put("useTicketCache", "true"); } else { // keytab options.put("useKeyTab", "true"); options.put("keyTab", this.keyTabLocation); options.put("principal", this.userPrincipal); options.put("storeKey", "true"); } options.put("doNotPrompt", "true"); options.put("isInitiator", "true"); if (loginOptions != null) { options.putAll(loginOptions); } return new AppConfigurationEntry[] { new AppConfigurationEntry( "com.sun.security.auth.module.Krb5LoginModule", AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options) }; } } private static class NullCredentials implements Credentials { @Override public Principal getUserPrincipal() { return null; } @Override public String getPassword() { return null; } } }