/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright (c) 2013-2015 Oracle and/or its affiliates. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common Development * and Distribution License("CDDL") (collectively, the "License"). You * may not use this file except in compliance with the License. You can * obtain a copy of the License at * http://glassfish.java.net/public/CDDL+GPL_1_1.html * or packager/legal/LICENSE.txt. See the License for the specific * language governing permissions and limitations under the License. * * When distributing the software, include this License Header Notice in each * file and include the License file at packager/legal/LICENSE.txt. * * GPL Classpath Exception: * Oracle designates this particular file as subject to the "Classpath" * exception as provided by Oracle in the GPL Version 2 section of the License * file that accompanied this code. * * Modifications: * If applicable, add the following below the License Header, with the fields * enclosed by brackets [] replaced by your own identifying information: * "Portions Copyright [year] [name of copyright owner]" * * Contributor(s): * If you wish your version of this file to be governed by only the CDDL or * only the GPL Version 2, indicate your decision by adding "[Contributor] * elects to include this software in this distribution under the [CDDL or GPL * Version 2] license." If you don't indicate a single choice of license, a * recipient has the option to distribute your version of this file under * either the CDDL, the GPL Version 2 or to extend the choice of license to * its licensees as provided above. However, if you add GPL Version 2 code * and therefore, elected the GPL Version 2 license, then the option applies * only if the new code is made subject to such option by the copyright * holder. */ package org.glassfish.jersey.client.authentication; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.nio.charset.Charset; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import javax.ws.rs.Priorities; import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.ClientRequestContext; import javax.ws.rs.client.ClientRequestFilter; import javax.ws.rs.client.ClientResponseContext; import javax.ws.rs.client.ClientResponseFilter; import javax.ws.rs.client.Entity; import javax.ws.rs.client.Invocation; import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.Configuration; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedHashMap; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.annotation.Priority; import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.client.internal.LocalizationMessages; /** * Http Authentication filter that provides basic and digest authentication (based on RFC 2617). * * @author Miroslav Fuksa */ @Priority(Priorities.AUTHENTICATION) class HttpAuthenticationFilter implements ClientRequestFilter, ClientResponseFilter { /** * Authentication type. */ enum Type { /** * Basic authentication. */ BASIC, /** * Digest authentication. */ DIGEST } private static final String REQUEST_PROPERTY_FILTER_REUSED = "org.glassfish.jersey.client.authentication.HttpAuthenticationFilter.reused"; private static final String REQUEST_PROPERTY_OPERATION = "org.glassfish.jersey.client.authentication.HttpAuthenticationFilter.operation"; /** * Encoding used for authentication calculations. */ static final Charset CHARACTER_SET = Charset.forName("iso-8859-1"); private final HttpAuthenticationFeature.Mode mode; /** * Cache with {@code URI:HTTP-METHOD} keys and authentication type as values. Contains successful * authentications already performed by the filter. */ private final Map<String, Type> uriCache; private final DigestAuthenticator digestAuth; private final BasicAuthenticator basicAuth; private static final int MAXIMUM_DIGEST_CACHE_SIZE = 10000; /** * Create a new filter instance. * * @param mode Mode. * @param basicCredentials Basic credentials (can be {@code null} if this filter does not work in the * basic mode or if no default credentials are defined). * @param digestCredentials Digest credentials (can be {@code null} if this filter does not work in the * digest mode or if no default credentials are defined). * @param configuration Configuration (non-{@code null}). */ HttpAuthenticationFilter(HttpAuthenticationFeature.Mode mode, Credentials basicCredentials, Credentials digestCredentials, Configuration configuration) { int limit = getMaximumCacheLimit(configuration); final int uriCacheLimit = limit * 2; // 2 is chosen to estimate there will be two times URIs // for basic and digest together than only digest // (limit estimates digest max URI number) uriCache = Collections.synchronizedMap(new LinkedHashMap<String, Type>(uriCacheLimit) { private static final long serialVersionUID = 1946245645415625L; @Override protected boolean removeEldestEntry(Map.Entry<String, Type> eldest) { return size() > uriCacheLimit; } }); this.mode = mode; switch (mode) { case BASIC_PREEMPTIVE: case BASIC_NON_PREEMPTIVE: this.basicAuth = new BasicAuthenticator(basicCredentials); this.digestAuth = null; break; case DIGEST: this.basicAuth = null; this.digestAuth = new DigestAuthenticator(digestCredentials, limit); break; case UNIVERSAL: this.basicAuth = new BasicAuthenticator(basicCredentials); this.digestAuth = new DigestAuthenticator(digestCredentials, limit); break; default: throw new IllegalStateException("Not implemented."); } } private int getMaximumCacheLimit(Configuration configuration) { int limit = ClientProperties.getValue(configuration.getProperties(), ClientProperties.DIGESTAUTH_URI_CACHE_SIZELIMIT, MAXIMUM_DIGEST_CACHE_SIZE); if (limit < 1) { limit = MAXIMUM_DIGEST_CACHE_SIZE; } return limit; } @Override public void filter(ClientRequestContext request) throws IOException { if ("true".equals(request.getProperty(REQUEST_PROPERTY_FILTER_REUSED))) { return; } if (request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) { return; } Type operation = null; if (mode == HttpAuthenticationFeature.Mode.BASIC_PREEMPTIVE) { basicAuth.filterRequest(request); operation = Type.BASIC; } else if (mode == HttpAuthenticationFeature.Mode.BASIC_NON_PREEMPTIVE) { // do nothing } else if (mode == HttpAuthenticationFeature.Mode.DIGEST) { if (digestAuth.filterRequest(request)) { operation = Type.DIGEST; } } else if (mode == HttpAuthenticationFeature.Mode.UNIVERSAL) { Type lastSuccessfulMethod = uriCache.get(getCacheKey(request)); if (lastSuccessfulMethod != null) { request.setProperty(REQUEST_PROPERTY_OPERATION, lastSuccessfulMethod); if (lastSuccessfulMethod == Type.BASIC) { basicAuth.filterRequest(request); operation = Type.BASIC; } else if (lastSuccessfulMethod == Type.DIGEST) { if (digestAuth.filterRequest(request)) { operation = Type.DIGEST; } } } } if (operation != null) { request.setProperty(REQUEST_PROPERTY_OPERATION, operation); } } @Override public void filter(ClientRequestContext request, ClientResponseContext response) throws IOException { if ("true".equals(request.getProperty(REQUEST_PROPERTY_FILTER_REUSED))) { return; } Type result = null; // which authentication is requested: BASIC or DIGEST boolean authenticate; if (response.getStatus() == Response.Status.UNAUTHORIZED.getStatusCode()) { String authString = response.getHeaders().getFirst(HttpHeaders.WWW_AUTHENTICATE); if (authString != null) { final String upperCaseAuth = authString.trim().toUpperCase(); if (upperCaseAuth.startsWith("BASIC")) { result = Type.BASIC; } else if (upperCaseAuth.startsWith("DIGEST")) { result = Type.DIGEST; } else { // unknown authentication -> this filter cannot authenticate with this method return; } } authenticate = true; } else { authenticate = false; } if (mode == HttpAuthenticationFeature.Mode.BASIC_PREEMPTIVE) { // do nothing -> 401 will be returned to the client } else if (mode == HttpAuthenticationFeature.Mode.BASIC_NON_PREEMPTIVE) { if (authenticate && result == Type.BASIC) { basicAuth.filterResponseAndAuthenticate(request, response); } } else if (mode == HttpAuthenticationFeature.Mode.DIGEST) { if (authenticate && result == Type.DIGEST) { digestAuth.filterResponse(request, response); } } else if (mode == HttpAuthenticationFeature.Mode.UNIVERSAL) { Type operation = (Type) request.getProperty(REQUEST_PROPERTY_OPERATION); if (operation != null) { updateCache(request, !authenticate, operation); } if (authenticate) { boolean success = false; // now we have the challenge response and we can authenticate if (result == Type.BASIC) { success = basicAuth.filterResponseAndAuthenticate(request, response); } else if (result == Type.DIGEST) { success = digestAuth.filterResponse(request, response); } updateCache(request, success, result); } } } private String getCacheKey(ClientRequestContext request) { return request.getUri().toString() + ":" + request.getMethod(); } private void updateCache(ClientRequestContext request, boolean success, Type operation) { String cacheKey = getCacheKey(request); if (success) { this.uriCache.put(cacheKey, operation); } else { this.uriCache.remove(cacheKey); } } /** * Repeat the {@code request} with provided {@code newAuthorizationHeader} * and update the {@code response} with newest response data. * * @param request Request context. * @param response Response context (will be updated with the new response data). * @param newAuthorizationHeader {@code Authorization} header that should be added to the new request. * @return {@code true} is the authentication was successful ({@code true} if 401 response code was not returned; * {@code false} otherwise). */ static boolean repeatRequest(ClientRequestContext request, ClientResponseContext response, String newAuthorizationHeader) { Client client = request.getClient(); String method = request.getMethod(); MediaType mediaType = request.getMediaType(); URI lUri = request.getUri(); WebTarget resourceTarget = client.target(lUri); Invocation.Builder builder = resourceTarget.request(mediaType); MultivaluedMap<String, Object> newHeaders = new MultivaluedHashMap<String, Object>(); for (Map.Entry<String, List<Object>> entry : request.getHeaders().entrySet()) { if (HttpHeaders.AUTHORIZATION.equals(entry.getKey())) { continue; } newHeaders.put(entry.getKey(), entry.getValue()); } newHeaders.add(HttpHeaders.AUTHORIZATION, newAuthorizationHeader); builder.headers(newHeaders); builder.property(REQUEST_PROPERTY_FILTER_REUSED, "true"); Invocation invocation; if (request.getEntity() == null) { invocation = builder.build(method); } else { invocation = builder.build(method, Entity.entity(request.getEntity(), request.getMediaType())); } Response nextResponse = invocation.invoke(); if (nextResponse.hasEntity()) { response.setEntityStream(nextResponse.readEntity(InputStream.class)); } MultivaluedMap<String, String> headers = response.getHeaders(); headers.clear(); headers.putAll(nextResponse.getStringHeaders()); response.setStatus(nextResponse.getStatus()); return response.getStatus() != Response.Status.UNAUTHORIZED.getStatusCode(); } /** * Credentials (username + password). */ static class Credentials { private final String username; private final byte[] password; /** * Create a new credentials from username and password as byte array. * * @param username Username. * @param password Password as byte array. */ Credentials(String username, byte[] password) { this.username = username; this.password = password; } /** * Create a new credentials from username and password as {@link String}. * * @param username Username. * @param password {@code String} password. */ Credentials(String username, String password) { this.username = username; this.password = password == null ? null : password.getBytes(CHARACTER_SET); } /** * Return username. * * @return username. */ String getUsername() { return username; } /** * Return password as byte array. * * @return Password string in byte array representation. */ byte[] getPassword() { return password; } } private static Credentials extractCredentials(ClientRequestContext request, Type type) { String usernameKey = null; String passwordKey = null; if (type == null) { usernameKey = HttpAuthenticationFeature.HTTP_AUTHENTICATION_USERNAME; passwordKey = HttpAuthenticationFeature.HTTP_AUTHENTICATION_PASSWORD; } else if (type == Type.BASIC) { usernameKey = HttpAuthenticationFeature.HTTP_AUTHENTICATION_BASIC_USERNAME; passwordKey = HttpAuthenticationFeature.HTTP_AUTHENTICATION_BASIC_PASSWORD; } else if (type == Type.DIGEST) { usernameKey = HttpAuthenticationFeature.HTTP_AUTHENTICATION_DIGEST_USERNAME; passwordKey = HttpAuthenticationFeature.HTTP_AUTHENTICATION_DIGEST_PASSWORD; } String userName = (String) request.getProperty(usernameKey); if (userName != null && !userName.equals("")) { byte[] pwdBytes; Object password = request.getProperty(passwordKey); if (password instanceof byte[]) { pwdBytes = ((byte[]) password); } else if (password instanceof String) { pwdBytes = ((String) password).getBytes(CHARACTER_SET); } else { throw new RequestAuthenticationException( LocalizationMessages.AUTHENTICATION_CREDENTIALS_REQUEST_PASSWORD_UNSUPPORTED()); } return new Credentials(userName, pwdBytes); } return null; } /** * Get credentials actual for the current request. Priorities in credentials selection are the following: * <ol> * <li>Basic/digest specific credentials defined in the request properties</li> * <li>Common credentials defined in the request properties</li> * <li>{@code defaultCredentials}</li> * </ol> * * @param request Request from which credentials should be extracted. * @param defaultCredentials Default credentials (can be {@code null}). * @param type Type of requested credentials. * @return Credentials or {@code null} if no credentials are found and {@code defaultCredentials} are {@code null}. * @throws RequestAuthenticationException in case the {@code username} or {@code password} is invalid. */ static Credentials getCredentials(ClientRequestContext request, Credentials defaultCredentials, Type type) { Credentials commonCredentials = extractCredentials(request, type); if (commonCredentials != null) { return commonCredentials; } else { Credentials specificCredentials = extractCredentials(request, null); return specificCredentials != null ? specificCredentials : defaultCredentials; } } }