| /** |
| * 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. See accompanying LICENSE file. |
| */ |
| package org.apache.hadoop.security.authentication.client; |
| |
| import org.apache.hadoop.security.authentication.server.AuthenticationFilter; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.net.CookieHandler; |
| import java.net.HttpCookie; |
| import java.net.HttpURLConnection; |
| import java.net.URI; |
| import java.net.URL; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * The {@link AuthenticatedURL} class enables the use of the JDK {@link URL} class |
| * against HTTP endpoints protected with the {@link AuthenticationFilter}. |
| * <p> |
| * The authentication mechanisms supported by default are Hadoop Simple authentication |
| * (also known as pseudo authentication) and Kerberos SPNEGO authentication. |
| * <p> |
| * Additional authentication mechanisms can be supported via {@link Authenticator} implementations. |
| * <p> |
| * The default {@link Authenticator} is the {@link KerberosAuthenticator} class which supports |
| * automatic fallback from Kerberos SPNEGO to Hadoop Simple authentication. |
| * <p> |
| * <code>AuthenticatedURL</code> instances are not thread-safe. |
| * <p> |
| * The usage pattern of the {@link AuthenticatedURL} is: |
| * <pre> |
| * |
| * // establishing an initial connection |
| * |
| * URL url = new URL("http://foo:8080/bar"); |
| * AuthenticatedURL.Token token = new AuthenticatedURL.Token(); |
| * AuthenticatedURL aUrl = new AuthenticatedURL(); |
| * HttpURLConnection conn = new AuthenticatedURL().openConnection(url, token); |
| * .... |
| * // use the 'conn' instance |
| * .... |
| * |
| * // establishing a follow up connection using a token from the previous connection |
| * |
| * HttpURLConnection conn = new AuthenticatedURL().openConnection(url, token); |
| * .... |
| * // use the 'conn' instance |
| * .... |
| * |
| * </pre> |
| */ |
| public class AuthenticatedURL { |
| private static final Logger LOG = |
| LoggerFactory.getLogger(AuthenticatedURL.class); |
| |
| /** |
| * Name of the HTTP cookie used for the authentication token between the client and the server. |
| */ |
| public static final String AUTH_COOKIE = "hadoop.auth"; |
| |
| // a lightweight cookie handler that will be attached to url connections. |
| // client code is not required to extract or inject auth cookies. |
| private static class AuthCookieHandler extends CookieHandler { |
| private HttpCookie authCookie; |
| private Map<String, List<String>> cookieHeaders = Collections.emptyMap(); |
| |
| @Override |
| public synchronized Map<String, List<String>> get(URI uri, |
| Map<String, List<String>> requestHeaders) throws IOException { |
| // call getter so it will reset headers if token is expiring. |
| getAuthCookie(); |
| return cookieHeaders; |
| } |
| |
| @Override |
| public void put(URI uri, Map<String, List<String>> responseHeaders) { |
| List<String> headers = responseHeaders.get("Set-Cookie"); |
| if (headers != null) { |
| for (String header : headers) { |
| List<HttpCookie> cookies; |
| try { |
| cookies = HttpCookie.parse(header); |
| } catch (IllegalArgumentException iae) { |
| // don't care. just skip malformed cookie headers. |
| LOG.debug("Cannot parse cookie header: " + header, iae); |
| continue; |
| } |
| for (HttpCookie cookie : cookies) { |
| if (AUTH_COOKIE.equals(cookie.getName())) { |
| setAuthCookie(cookie); |
| } |
| } |
| } |
| } |
| } |
| |
| // return the auth cookie if still valid. |
| private synchronized HttpCookie getAuthCookie() { |
| if (authCookie != null && authCookie.hasExpired()) { |
| setAuthCookie(null); |
| } |
| return authCookie; |
| } |
| |
| private synchronized void setAuthCookie(HttpCookie cookie) { |
| final HttpCookie oldCookie = authCookie; |
| // will redefine if new cookie is valid. |
| authCookie = null; |
| cookieHeaders = Collections.emptyMap(); |
| boolean valid = cookie != null && !cookie.getValue().isEmpty() && |
| !cookie.hasExpired(); |
| if (valid) { |
| // decrease lifetime to avoid using a cookie soon to expire. |
| // allows authenticators to pre-emptively reauthenticate to |
| // prevent clients unnecessarily receiving a 401. |
| long maxAge = cookie.getMaxAge(); |
| if (maxAge != -1) { |
| cookie.setMaxAge(maxAge * 9/10); |
| valid = !cookie.hasExpired(); |
| } |
| } |
| if (valid) { |
| // v0 cookies value aren't quoted by default but tomcat demands |
| // quoting. |
| if (cookie.getVersion() == 0) { |
| String value = cookie.getValue(); |
| if (!value.startsWith("\"")) { |
| value = "\"" + value + "\""; |
| cookie.setValue(value); |
| } |
| } |
| authCookie = cookie; |
| cookieHeaders = new HashMap<>(); |
| cookieHeaders.put("Cookie", Arrays.asList(cookie.toString())); |
| } |
| LOG.trace("Setting token value to {} ({})", authCookie, oldCookie); |
| } |
| |
| private void setAuthCookieValue(String value) { |
| HttpCookie c = null; |
| if (value != null) { |
| c = new HttpCookie(AUTH_COOKIE, value); |
| } |
| setAuthCookie(c); |
| } |
| } |
| |
| /** |
| * Client side authentication token. |
| */ |
| public static class Token { |
| |
| private final AuthCookieHandler cookieHandler = new AuthCookieHandler(); |
| |
| /** |
| * Creates a token. |
| */ |
| public Token() { |
| } |
| |
| /** |
| * Creates a token using an existing string representation of the token. |
| * |
| * @param tokenStr string representation of the tokenStr. |
| */ |
| public Token(String tokenStr) { |
| if (tokenStr == null) { |
| throw new IllegalArgumentException("tokenStr cannot be null"); |
| } |
| set(tokenStr); |
| } |
| |
| /** |
| * Returns if a token from the server has been set. |
| * |
| * @return if a token from the server has been set. |
| */ |
| public boolean isSet() { |
| return cookieHandler.getAuthCookie() != null; |
| } |
| |
| /** |
| * Sets a token. |
| * |
| * @param tokenStr string representation of the tokenStr. |
| */ |
| void set(String tokenStr) { |
| cookieHandler.setAuthCookieValue(tokenStr); |
| } |
| |
| /** |
| * Installs a cookie handler for the http request to manage session |
| * cookies. |
| * @param url |
| * @return HttpUrlConnection |
| * @throws IOException |
| */ |
| HttpURLConnection openConnection(URL url, |
| ConnectionConfigurator connConfigurator) throws IOException { |
| // the cookie handler is unfortunately a global static. it's a |
| // synchronized class method so we can safely swap the handler while |
| // instantiating the connection object to prevent it leaking into |
| // other connections. |
| final HttpURLConnection conn; |
| synchronized(CookieHandler.class) { |
| CookieHandler current = CookieHandler.getDefault(); |
| CookieHandler.setDefault(cookieHandler); |
| try { |
| conn = (HttpURLConnection)url.openConnection(); |
| } finally { |
| CookieHandler.setDefault(current); |
| } |
| } |
| if (connConfigurator != null) { |
| connConfigurator.configure(conn); |
| } |
| return conn; |
| } |
| |
| /** |
| * Returns the string representation of the token. |
| * |
| * @return the string representation of the token. |
| */ |
| @Override |
| public String toString() { |
| String value = ""; |
| HttpCookie authCookie = cookieHandler.getAuthCookie(); |
| if (authCookie != null) { |
| value = authCookie.getValue(); |
| if (value.startsWith("\"")) { // tests don't want the quotes. |
| value = value.substring(1, value.length()-1); |
| } |
| } |
| return value; |
| } |
| |
| } |
| |
| private static Class<? extends Authenticator> DEFAULT_AUTHENTICATOR = KerberosAuthenticator.class; |
| |
| /** |
| * Sets the default {@link Authenticator} class to use when an {@link AuthenticatedURL} instance |
| * is created without specifying an authenticator. |
| * |
| * @param authenticator the authenticator class to use as default. |
| */ |
| public static void setDefaultAuthenticator(Class<? extends Authenticator> authenticator) { |
| DEFAULT_AUTHENTICATOR = authenticator; |
| } |
| |
| /** |
| * Returns the default {@link Authenticator} class to use when an {@link AuthenticatedURL} instance |
| * is created without specifying an authenticator. |
| * |
| * @return the authenticator class to use as default. |
| */ |
| public static Class<? extends Authenticator> getDefaultAuthenticator() { |
| return DEFAULT_AUTHENTICATOR; |
| } |
| |
| private Authenticator authenticator; |
| private ConnectionConfigurator connConfigurator; |
| |
| /** |
| * Creates an {@link AuthenticatedURL}. |
| */ |
| public AuthenticatedURL() { |
| this(null); |
| } |
| |
| /** |
| * Creates an <code>AuthenticatedURL</code>. |
| * |
| * @param authenticator the {@link Authenticator} instance to use, if <code>null</code> a {@link |
| * KerberosAuthenticator} is used. |
| */ |
| public AuthenticatedURL(Authenticator authenticator) { |
| this(authenticator, null); |
| } |
| |
| /** |
| * Creates an <code>AuthenticatedURL</code>. |
| * |
| * @param authenticator the {@link Authenticator} instance to use, if <code>null</code> a {@link |
| * KerberosAuthenticator} is used. |
| * @param connConfigurator a connection configurator. |
| */ |
| public AuthenticatedURL(Authenticator authenticator, |
| ConnectionConfigurator connConfigurator) { |
| try { |
| this.authenticator = (authenticator != null) ? authenticator : DEFAULT_AUTHENTICATOR.newInstance(); |
| } catch (Exception ex) { |
| throw new RuntimeException(ex); |
| } |
| this.connConfigurator = connConfigurator; |
| this.authenticator.setConnectionConfigurator(connConfigurator); |
| } |
| |
| /** |
| * Returns the {@link Authenticator} instance used by the |
| * <code>AuthenticatedURL</code>. |
| * |
| * @return the {@link Authenticator} instance |
| */ |
| protected Authenticator getAuthenticator() { |
| return authenticator; |
| } |
| |
| /** |
| * Returns an authenticated {@link HttpURLConnection}. |
| * |
| * @param url the URL to connect to. Only HTTP/S URLs are supported. |
| * @param token the authentication token being used for the user. |
| * |
| * @return an authenticated {@link HttpURLConnection}. |
| * |
| * @throws IOException if an IO error occurred. |
| * @throws AuthenticationException if an authentication exception occurred. |
| */ |
| public HttpURLConnection openConnection(URL url, Token token) throws IOException, AuthenticationException { |
| if (url == null) { |
| throw new IllegalArgumentException("url cannot be NULL"); |
| } |
| if (!url.getProtocol().equalsIgnoreCase("http") && !url.getProtocol().equalsIgnoreCase("https")) { |
| throw new IllegalArgumentException("url must be for a HTTP or HTTPS resource"); |
| } |
| if (token == null) { |
| throw new IllegalArgumentException("token cannot be NULL"); |
| } |
| authenticator.authenticate(url, token); |
| |
| // allow the token to create the connection with a cookie handler for |
| // managing session cookies. |
| return token.openConnection(url, connConfigurator); |
| } |
| |
| /** |
| * Helper method that injects an authentication token to send with a |
| * connection. Callers should prefer using |
| * {@link Token#openConnection(URL, ConnectionConfigurator)} which |
| * automatically manages authentication tokens. |
| * |
| * @param conn connection to inject the authentication token into. |
| * @param token authentication token to inject. |
| */ |
| public static void injectToken(HttpURLConnection conn, Token token) { |
| HttpCookie authCookie = token.cookieHandler.getAuthCookie(); |
| if (authCookie != null) { |
| conn.addRequestProperty("Cookie", authCookie.toString()); |
| } |
| } |
| |
| /** |
| * Helper method that extracts an authentication token received from a connection. |
| * <p> |
| * This method is used by {@link Authenticator} implementations. |
| * |
| * @param conn connection to extract the authentication token from. |
| * @param token the authentication token. |
| * |
| * @throws IOException if an IO error occurred. |
| * @throws AuthenticationException if an authentication exception occurred. |
| */ |
| public static void extractToken(HttpURLConnection conn, Token token) throws IOException, AuthenticationException { |
| int respCode = conn.getResponseCode(); |
| if (respCode == HttpURLConnection.HTTP_OK |
| || respCode == HttpURLConnection.HTTP_CREATED |
| || respCode == HttpURLConnection.HTTP_ACCEPTED) { |
| // cookie handler should have already extracted the token. try again |
| // for backwards compatibility if this method is called on a connection |
| // not opened via this instance. |
| token.cookieHandler.put(null, conn.getHeaderFields()); |
| } else if (respCode == HttpURLConnection.HTTP_NOT_FOUND) { |
| LOG.trace("Setting token value to null ({}), resp={}", token, respCode); |
| token.set(null); |
| throw new FileNotFoundException(conn.getURL().toString()); |
| } else { |
| LOG.trace("Setting token value to null ({}), resp={}", token, respCode); |
| token.set(null); |
| throw new AuthenticationException("Authentication failed" + |
| ", URL: " + conn.getURL() + |
| ", status: " + conn.getResponseCode() + |
| ", message: " + conn.getResponseMessage()); |
| } |
| } |
| |
| } |