All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.hadoop.security.authentication.client.AuthenticatedURL Maven / Gradle / Ivy

There is a newer version: 3.4.1
Show newest version
/**
 * 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}.
 * 

* The authentication mechanisms supported by default are Hadoop Simple authentication * (also known as pseudo authentication) and Kerberos SPNEGO authentication. *

* Additional authentication mechanisms can be supported via {@link Authenticator} implementations. *

* The default {@link Authenticator} is the {@link KerberosAuthenticator} class which supports * automatic fallback from Kerberos SPNEGO to Hadoop Simple authentication. *

* AuthenticatedURL instances are not thread-safe. *

* The usage pattern of the {@link AuthenticatedURL} is: *

 *
 * // 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
 * ....
 *
 * 
*/ 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> cookieHeaders = Collections.emptyMap(); @Override public synchronized Map> get(URI uri, Map> requestHeaders) throws IOException { // call getter so it will reset headers if token is expiring. getAuthCookie(); return cookieHeaders; } @Override public void put(URI uri, Map> responseHeaders) { List headers = responseHeaders.get("Set-Cookie"); if (headers != null) { for (String header : headers) { List 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 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 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 getDefaultAuthenticator() { return DEFAULT_AUTHENTICATOR; } private Authenticator authenticator; private ConnectionConfigurator connConfigurator; /** * Creates an {@link AuthenticatedURL}. */ public AuthenticatedURL() { this(null); } /** * Creates an AuthenticatedURL. * * @param authenticator the {@link Authenticator} instance to use, if null a {@link * KerberosAuthenticator} is used. */ public AuthenticatedURL(Authenticator authenticator) { this(authenticator, null); } /** * Creates an AuthenticatedURL. * * @param authenticator the {@link Authenticator} instance to use, if null 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 * AuthenticatedURL. * * @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. *

* 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()); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy