org.glassfish.jersey.client.authentication.DigestAuthenticator Maven / Gradle / Ivy
/*
* Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
package org.glassfish.jersey.client.authentication;
import java.io.IOException;
import java.net.URI;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientResponseContext;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import org.glassfish.jersey.client.internal.LocalizationMessages;
import org.glassfish.jersey.message.MessageUtils;
import org.glassfish.jersey.uri.UriComponent;
/**
* Implementation of Digest Http Authentication method (RFC 2617).
*
* @author [email protected]
* @author Stefan Katerkamp ([email protected])
* @author Miroslav Fuksa
*/
final class DigestAuthenticator {
private static final char[] HEX_ARRAY = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
private static final Pattern KEY_VALUE_PAIR_PATTERN = Pattern.compile("(\\w+)\\s*=\\s*(\"([^\"]+)\"|(\\w+))\\s*,?\\s*");
private static final int CLIENT_NONCE_BYTE_COUNT = 4;
private final SecureRandom randomGenerator;
private final HttpAuthenticationFilter.Credentials credentials;
private final Map digestCache;
/**
* Create a new instance initialized from credentials and configuration.
*
* @param credentials Credentials. Can be {@code null} if there are no default credentials.
* @param limit Maximum number of URIs that should be kept in the cache containing URIs and their
* {@link org.glassfish.jersey.client.authentication.DigestAuthenticator.DigestScheme}.
*/
DigestAuthenticator(final HttpAuthenticationFilter.Credentials credentials, final int limit) {
this.credentials = credentials;
digestCache = Collections.synchronizedMap(new LinkedHashMap(limit) {
// use id as it is an anonymous inner class with changed behaviour
private static final long serialVersionUID = 2546245625L;
@Override
protected boolean removeEldestEntry(final Map.Entry eldest) {
return size() > limit;
}
});
try {
randomGenerator = SecureRandom.getInstance("SHA1PRNG");
} catch (final NoSuchAlgorithmException e) {
throw new RequestAuthenticationException(LocalizationMessages.ERROR_DIGEST_FILTER_GENERATOR(), e);
}
}
/**
* Process request and add authentication information if possible.
*
* @param request Request context.
* @return {@code true} if authentication information was added.
* @throws IOException When error with encryption occurs.
*/
boolean filterRequest(final ClientRequestContext request) throws IOException {
final DigestScheme digestScheme = digestCache.get(AuthenticationUtil.getCacheKey(request));
if (digestScheme != null) {
final HttpAuthenticationFilter.Credentials cred = HttpAuthenticationFilter.getCredentials(request,
this.credentials, HttpAuthenticationFilter.Type.DIGEST);
if (cred != null) {
request.getHeaders().add(HttpHeaders.AUTHORIZATION, createNextAuthToken(digestScheme, request, cred));
return true;
}
}
return false;
}
/**
* Process response and repeat the request if digest authentication is requested. When request is repeated
* the response will be modified to contain new response information.
*
* @param request Request context.
* @param response Response context (will be updated with newest response data if the request was repeated).
* @return {@code true} if response does not require authentication or if authentication is required,
* new request was done with digest authentication information and authentication was successful.
* @throws IOException When error with encryption occurs.
*/
public boolean filterResponse(final ClientRequestContext request, final ClientResponseContext response) throws IOException {
if (Response.Status.fromStatusCode(response.getStatus()) == Response.Status.UNAUTHORIZED) {
final DigestScheme digestScheme = parseAuthHeaders(response.getHeaders().get(HttpHeaders.WWW_AUTHENTICATE));
if (digestScheme == null) {
return false;
}
// assemble authentication request and resend it
final HttpAuthenticationFilter.Credentials cred = HttpAuthenticationFilter.getCredentials(request,
this.credentials, HttpAuthenticationFilter.Type.DIGEST);
if (cred == null) {
if (response.hasEntity()) {
AuthenticationUtil.discardInputAndClose(response.getEntityStream());
}
throw new ResponseAuthenticationException(null, LocalizationMessages.AUTHENTICATION_CREDENTIALS_MISSING_DIGEST());
}
final boolean success = HttpAuthenticationFilter.repeatRequest(request, response, createNextAuthToken(digestScheme,
request, cred));
URI cacheKey = AuthenticationUtil.getCacheKey(request);
if (success) {
digestCache.put(cacheKey, digestScheme);
} else {
digestCache.remove(cacheKey);
}
return success;
}
return true;
}
/**
* Parse digest header.
*
* @param headers List of header strings
* @return DigestScheme or {@code null} if no digest header exists.
*/
private DigestScheme parseAuthHeaders(final List> headers) throws IOException {
if (headers == null) {
return null;
}
for (final Object lineObject : headers) {
if (!(lineObject instanceof String)) {
continue;
}
final String line = (String) lineObject;
final String[] parts = line.trim().split("\\s+", 2);
if (parts.length != 2) {
continue;
}
if (!"digest".equals(parts[0].toLowerCase())) {
continue;
}
String realm = null;
String nonce = null;
String opaque = null;
QOP qop = QOP.UNSPECIFIED;
Algorithm algorithm = Algorithm.UNSPECIFIED;
boolean stale = false;
final Matcher match = KEY_VALUE_PAIR_PATTERN.matcher(parts[1]);
while (match.find()) {
// expect 4 groups (key)=("(val)" | (val))
final int nbGroups = match.groupCount();
if (nbGroups != 4) {
continue;
}
final String key = match.group(1);
final String valNoQuotes = match.group(3);
final String valQuotes = match.group(4);
final String val = (valNoQuotes == null) ? valQuotes : valNoQuotes;
if ("qop".equals(key)) {
qop = QOP.parse(val);
} else if ("realm".equals(key)) {
realm = val;
} else if ("nonce".equals(key)) {
nonce = val;
} else if ("opaque".equals(key)) {
opaque = val;
} else if ("stale".equals(key)) {
stale = Boolean.parseBoolean(val);
} else if ("algorithm".equals(key)) {
algorithm = Algorithm.parse(val);
}
}
return new DigestScheme(realm, nonce, opaque, qop, algorithm, stale);
}
return null;
}
/**
* Creates digest string including counter.
*
* @param ds DigestScheme instance
* @param requestContext client request context
* @return digest authentication token string
* @throws IOException
*/
private String createNextAuthToken(final DigestScheme ds, final ClientRequestContext requestContext,
final HttpAuthenticationFilter.Credentials credentials) throws IOException {
final StringBuilder sb = new StringBuilder(100);
sb.append("Digest ");
append(sb, "username", credentials.getUsername());
append(sb, "realm", ds.getRealm());
append(sb, "nonce", ds.getNonce());
append(sb, "opaque", ds.getOpaque());
append(sb, "algorithm", ds.getAlgorithm().toString(), false);
append(sb, "qop", ds.getQop().toString(), false);
final String uri = UriComponent.fullRelativeUri(requestContext.getUri());
append(sb, "uri", uri);
final String ha1;
if (ds.getAlgorithm() == Algorithm.MD5_SESS) {
ha1 = md5(md5(credentials.getUsername(), ds.getRealm(),
new String(credentials.getPassword(), MessageUtils.getCharset(requestContext.getMediaType()))));
} else {
ha1 = md5(credentials.getUsername(), ds.getRealm(),
new String(credentials.getPassword(), MessageUtils.getCharset(requestContext.getMediaType())));
}
final String ha2 = md5(requestContext.getMethod(), uri);
final String response;
if (ds.getQop() == QOP.UNSPECIFIED) {
response = md5(ha1, ds.getNonce(), ha2);
} else {
final String cnonce = randomBytes(CLIENT_NONCE_BYTE_COUNT); // client nonce
append(sb, "cnonce", cnonce);
final String nc = String.format("%08x", ds.incrementCounter()); // counter
append(sb, "nc", nc, false);
response = md5(ha1, ds.getNonce(), nc, cnonce, ds.getQop().toString(), ha2);
}
append(sb, "response", response);
return sb.toString();
}
/**
* Append comma separated key=value token
*
* @param sb string builder instance
* @param key key string
* @param value value string
* @param useQuote true if value needs to be enclosed in quotes
*/
private static void append(final StringBuilder sb, final String key, final String value, final boolean useQuote) {
if (value == null) {
return;
}
if (sb.length() > 0) {
if (sb.charAt(sb.length() - 1) != ' ') {
sb.append(',');
}
}
sb.append(key);
sb.append('=');
if (useQuote) {
sb.append('"');
}
sb.append(value);
if (useQuote) {
sb.append('"');
}
}
/**
* Append comma separated key=value token. The value gets enclosed in
* quotes.
*
* @param sb string builder instance
* @param key key string
* @param value value string
*/
private static void append(final StringBuilder sb, final String key, final String value) {
append(sb, key, value, true);
}
/**
* 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);
}
/**
* Colon separated value MD5 hash.
*
* @param tokens one or more strings
* @return M5 hash string
* @throws IOException
*/
private static String md5(final String... tokens) throws IOException {
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 IOException(ex.getMessage());
}
md.update(sb.toString().getBytes(HttpAuthenticationFilter.CHARACTER_SET), 0, sb.length());
final byte[] md5hash = md.digest();
return bytesToHex(md5hash);
}
/**
* Generate a random sequence of bytes and return its hex representation
*
* @param nbBytes number of bytes to generate
* @return hex string
*/
private String randomBytes(final int nbBytes) {
final byte[] bytes = new byte[nbBytes];
randomGenerator.nextBytes(bytes);
return bytesToHex(bytes);
}
private enum QOP {
UNSPECIFIED(null),
AUTH("auth");
private final String qop;
QOP(final String qop) {
this.qop = qop;
}
@Override
public String toString() {
return qop;
}
public static QOP parse(final String val) {
if (val == null || val.isEmpty()) {
return QOP.UNSPECIFIED;
}
if (val.contains("auth")) {
return QOP.AUTH;
}
throw new UnsupportedOperationException(LocalizationMessages.DIGEST_FILTER_QOP_UNSUPPORTED(val));
}
}
enum Algorithm {
UNSPECIFIED(null),
MD5("MD5"),
MD5_SESS("MD5-sess");
private final String md;
Algorithm(final String md) {
this.md = md;
}
@Override
public String toString() {
return md;
}
public static Algorithm parse(String val) {
if (val == null || val.isEmpty()) {
return Algorithm.UNSPECIFIED;
}
val = val.trim();
if (val.contains(MD5_SESS.md) || val.contains(MD5_SESS.md.toLowerCase())) {
return MD5_SESS;
}
return MD5;
}
}
/**
* Digest scheme POJO
*/
final class DigestScheme {
private final String realm;
private final String nonce;
private final String opaque;
private final Algorithm algorithm;
private final QOP qop;
private final boolean stale;
private volatile int nc;
DigestScheme(final String realm, final String nonce, final String opaque, final QOP qop, final Algorithm algorithm,
final boolean stale) {
this.realm = realm;
this.nonce = nonce;
this.opaque = opaque;
this.qop = qop;
this.algorithm = algorithm;
this.stale = stale;
this.nc = 0;
}
public int incrementCounter() {
return ++nc;
}
public String getNonce() {
return nonce;
}
public String getRealm() {
return realm;
}
public String getOpaque() {
return opaque;
}
public Algorithm getAlgorithm() {
return algorithm;
}
public QOP getQop() {
return qop;
}
public boolean isStale() {
return stale;
}
public int getNc() {
return nc;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy