/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright (c) 2010-2017 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.tests.e2e.client; import java.nio.charset.Charset; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.List; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.ProcessingException; import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.Application; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.ResponseBuilder; import javax.ws.rs.core.UriInfo; import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.test.JerseyTest; import org.glassfish.jersey.test.TestProperties; import org.glassfish.jersey.uri.UriComponent; import org.junit.Assert; import org.junit.Test; /** * * @author Pavel Bucek (pavel.bucek at oracle.com) * @author Stefan Katerkamp <stefan at katerkamp.de> */ public class HttpDigestAuthFilterTest extends JerseyTest { private static final String DIGEST_TEST_LOGIN = "user"; private static final String DIGEST_TEST_PASS = "password"; private static final String DIGEST_TEST_INVALIDPASS = "nopass"; // Digest string expected for OK auth: // Digest realm="test", nonce="eDePFNeJBAA=a874814ec55647862b66a747632603e5825acd39", // algorithm=MD5, domain="/auth-digest/", qop="auth" private static final String DIGEST_TEST_NONCE = "eDePFNeJBAA=a874814ec55647862b66a747632603e5825acd39"; private static final String DIGEST_TEST_REALM = "test"; private static final String DIGEST_TEST_DOMAIN = "/auth-digest/"; private static int ncExpected = 1; @Override protected Application configure() { enable(TestProperties.LOG_TRAFFIC); enable(TestProperties.DUMP_ENTITY); return new ResourceConfig(Resource.class); } @Path("/auth-digest") public static class Resource { @Context private HttpHeaders httpHeaders; @Context private UriInfo uriInfo; @GET public Response get1() { return verify(); } @GET @Path("ěščřžýáíé") public Response getEncoding() { return verify(); } private Response verify() { if (httpHeaders.getRequestHeader(HttpHeaders.AUTHORIZATION) == null) { // the first request has no authorization header, tell filter its 401 // and send filter back seed for the new to be built header ResponseBuilder responseBuilder = Response.status(Response.Status.UNAUTHORIZED); responseBuilder = responseBuilder.header(HttpHeaders.WWW_AUTHENTICATE, "Digest realm=\"" + DIGEST_TEST_REALM + "\", " + "nonce=\"" + DIGEST_TEST_NONCE + "\", " + "algorithm=MD5, " + "domain=\"" + DIGEST_TEST_DOMAIN + "\", qop=\"auth\""); return responseBuilder.build(); } else { // the filter takes the seed and adds the header final List<String> authList = httpHeaders.getRequestHeader(HttpHeaders.AUTHORIZATION); if (authList.size() != 1) { return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); } final String authHeader = authList.get(0); final String ha1 = md5(DIGEST_TEST_LOGIN, DIGEST_TEST_REALM, DIGEST_TEST_PASS); final String requestUri = UriComponent.fullRelativeUri(uriInfo.getRequestUri()); final String ha2 = md5("GET", requestUri.startsWith("/") ? requestUri : "/" + requestUri); final String response = md5( ha1, DIGEST_TEST_NONCE, getDigestAuthHeaderValue(authHeader, "nc="), getDigestAuthHeaderValue(authHeader, "cnonce="), getDigestAuthHeaderValue(authHeader, "qop="), ha2); // this generates INTERNAL_SERVER_ERROR if not matching Assert.assertEquals(ncExpected, Integer.parseInt(getDigestAuthHeaderValue(authHeader, "nc="))); if (response.equals(getDigestAuthHeaderValue(authHeader, "response="))) { return Response.ok().build(); } else { return Response.status(Response.Status.UNAUTHORIZED).build(); } } } private static final Charset CHARACTER_SET = Charset.forName("iso-8859-1"); /** * Colon separated value MD5 hash. Call md5 method of the filter. * * @param tokens one or more strings * @return M5 hash string */ static String md5(final String... tokens) { final StringBuilder sb = new StringBuilder(100); for (final String token : tokens) { if (sb.length() > 0) { sb.append(':'); } sb.append(token); } final MessageDigest md; try { md = MessageDigest.getInstance("MD5"); } catch (final NoSuchAlgorithmException ex) { throw new ProcessingException(ex.getMessage()); } md.update(sb.toString().getBytes(CHARACTER_SET), 0, sb.length()); final byte[] md5hash = md.digest(); return bytesToHex(md5hash); } /** * Convert bytes array to hex string. * * @param bytes array of bytes * @return hex string */ private static String bytesToHex(final byte[] bytes) { final char[] hexChars = new char[bytes.length * 2]; int v; for (int j = 0; j < bytes.length; j++) { v = bytes[j] & 0xFF; hexChars[j * 2] = HEX_ARRAY[v >>> 4]; hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; } return new String(hexChars); } private static final char[] HEX_ARRAY = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; /** * Get a value of the Digest Auth Header. * * @param authHeader digest auth header string * @param keyName key of the value to retrieve * @return value string */ static String getDigestAuthHeaderValue(final String authHeader, final String keyName) { final int i1 = authHeader.indexOf(keyName); if (i1 == -1) { return null; } String value = authHeader.substring( authHeader.indexOf('=', i1) + 1, (authHeader.indexOf(',', i1) != -1 ? authHeader.indexOf(',', i1) : authHeader.length())); value = value.trim(); if (value.charAt(0) == '"' && value.charAt(value.length() - 1) == '"') { value = value.substring(1, value.length() - 1); } return value; } } @Test public void testHttpDigestAuthFilter() { testRequest("auth-digest"); } @Test public void testHttpDigestAuthFilterWithEncodedUri() { testRequest("auth-digest/ěščřžýáíé"); } @Test public void testHttpDigestAuthFilterWithParams() { testRequest("auth-digest", true); } @Test public void testHttpDigestAuthFilterWithEncodedUriAndParams() { testRequest("auth-digest/ěščřžýáíé", true); } private void testRequest(final String path) { testRequest(path, false); } private void testRequest(final String path, final boolean addParams) { WebTarget resource = target() .register(HttpAuthenticationFeature.digest(DIGEST_TEST_LOGIN, DIGEST_TEST_PASS)) .path(path); if (addParams) { resource = resource.matrixParam("bar", "foo").queryParam("foo", "bar"); } ncExpected = 1; final Response r1 = resource.request().get(); Assert.assertEquals(Response.Status.fromStatusCode(r1.getStatus()), Response.Status.OK); } @Test public void testPreemptive() { final WebTarget resource = target() .register(HttpAuthenticationFeature.digest(DIGEST_TEST_LOGIN, DIGEST_TEST_PASS)) .path("auth-digest"); ncExpected = 1; final Response r1 = resource.request().get(); Assert.assertEquals(Response.Status.fromStatusCode(r1.getStatus()), Response.Status.OK); ncExpected = 2; final Response r2 = resource.request().get(); Assert.assertEquals(Response.Status.fromStatusCode(r2.getStatus()), Response.Status.OK); ncExpected = 3; final Response r3 = resource.request().get(); Assert.assertEquals(Response.Status.fromStatusCode(r3.getStatus()), Response.Status.OK); } @Test public void testAuthentication() { final WebTarget resource = target() .register(HttpAuthenticationFeature.digest(DIGEST_TEST_LOGIN, DIGEST_TEST_INVALIDPASS)) .path("auth-digest"); ncExpected = 1; final Response r1 = resource.request().get(); Assert.assertEquals(Response.Status.fromStatusCode(r1.getStatus()), Response.Status.UNAUTHORIZED); } }