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