/*
 * Copyright (c) 2013, 2019 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.Locale;
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:raphael.jolivet@gmail.com, Stefan Katerkamp (stefan@katerkamp.de), Miroslav Fuksa
/** * Implementation of Digest Http Authentication method (RFC 2617). * * @author raphael.jolivet@gmail.com * @author Stefan Katerkamp (stefan@katerkamp.de) * @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<URI, DigestScheme> digestCache;
Create a new instance initialized from credentials and configuration.
Params:
  • credentials – Credentials. Can be null if there are no default credentials.
  • limit – Maximum number of URIs that should be kept in the cache containing URIs and their DigestScheme.
/** * 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<URI, DigestScheme>(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.
Params:
  • request – Request context.
Throws:
Returns:true if authentication information was added.
/** * 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.
Params:
  • request – Request context.
  • response – Response context (will be updated with newest response data if the request was repeated).
Throws:
Returns:true if response does not require authentication or if authentication is required, new request was done with digest authentication information and authentication was successful.
/** * 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.
Params:
  • headers – List of header strings
Returns:DigestScheme or null if no digest header exists.
/** * 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(Locale.ROOT))) { 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.
Params:
  • ds – DigestScheme instance
  • requestContext – client request context
Throws:
Returns:digest authentication token string
/** * 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
Params:
  • sb – string builder instance
  • key – key string
  • value – value string
  • useQuote – true if value needs to be enclosed in quotes
/** * 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.
Params:
  • sb – string builder instance
  • key – key string
  • value – value string
/** * 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.
Params:
  • bytes – array of bytes
Returns:hex string
/** * 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.
Params:
  • tokens – one or more strings
Throws:
Returns:M5 hash string
/** * 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
Params:
  • nbBytes – number of bytes to generate
Returns:hex string
/** * 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(Locale.ROOT))) { return MD5_SESS; } return MD5; } }
Digest scheme POJO
/** * 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; } } }