org.apache.catalina.authenticator.DigestAuthenticator Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of payara-micro Show documentation
Show all versions of payara-micro Show documentation
Micro Distribution of the Payara Project
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 1997-2013 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
* https://glassfish.dev.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.
*
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2004 The Apache Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Portions Copyright [2018-2021] [Payara Foundation and/or its affiliates]
package org.apache.catalina.authenticator;
import static jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
import static org.apache.catalina.authenticator.Constants.DIGEST_METHOD;
import static org.apache.catalina.authenticator.Constants.REQ_SSOID_NOTE;
import java.io.IOException;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.util.StringTokenizer;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.catalina.HttpRequest;
import org.apache.catalina.HttpResponse;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.deploy.LoginConfig;
import org.apache.catalina.util.DigestEncoder;
/**
* An Authenticator and Valve implementation of HTTP DIGEST Authentication (see RFC 2069).
*
* @author Craig R. McClanahan
* @author Remy Maucherat
* @version $Revision: 1.6 $ $Date: 2007/04/17 21:33:22 $
*/
public class DigestAuthenticator extends AuthenticatorBase {
// -------------------------------------------------------------- Constants
/**
* The MD5 helper object for this class.
*/
protected static final DigestEncoder digestEncoder = new DigestEncoder();
/**
* Descriptive information about this implementation.
*/
protected static final String info = "org.apache.catalina.authenticator.DigestAuthenticator/1.0";
/**
* DIGEST implementation only supports auth quality of protection.
*/
protected static final String QOP = "auth";
/**
* The default message digest algorithm to use if we cannot use the requested one.
*/
protected static final String DEFAULT_ALGORITHM = "MD5";
private static final String EMPTY_STRING = "";
// ----------------------------------------------------------- Constructors
public DigestAuthenticator() {
super();
}
// ----------------------------------------------------- Static Variables
/**
* The message digest algorithm to be used when generating session identifiers. This must be an algorithm supported by
* the java.security.MessageDigest
class on your platform.
*/
protected static volatile String algorithm = DEFAULT_ALGORITHM;
/**
* MD5 message digest provider.
*/
protected static volatile MessageDigest messageDigest;
// ----------------------------------------------------- Instance Variables
/**
* Maximum number of client nonces to keep in the cache. If not specified, the default value of 1000 is used.
*/
protected int cnonceCacheSize = 1000;
/**
* Private key.
*/
protected String key;
/**
* How long server nonces are valid for in milliseconds. Defaults to 5 minutes.
*/
protected long nonceValidity = 5 * 60 * 1000;
/**
* Opaque string.
*/
protected String opaque;
/**
* Should the URI be validated as required by RFC2617? Can be disabled in reverse proxies where the proxy has modified
* the URI.
*/
protected boolean validateUri = true;
// --------------------------------------------------------- Public Methods
/**
* Authenticate the user making this request, based on the specified login configuration. Return true
if
* any specified constraint has been satisfied, or false
if we have created a response challenge already.
*
* @param request Request we are processing
* @param response Response we are creating
* @param config Login configuration describing how authentication should be performed
*
* @exception IOException if an input/output error occurs
*/
@Override
public boolean authenticate(HttpRequest request, HttpResponse response, LoginConfig config) throws IOException {
// Have we already authenticated someone?
Principal principal = ((HttpServletRequest) request.getRequest()).getUserPrincipal();
if (principal != null) {
return true;
}
// Validate any credentials already included with this request
HttpServletRequest httpServletRequest = (HttpServletRequest) request.getRequest();
HttpServletResponse httpServletResponse = (HttpServletResponse) response.getResponse();
String authorization = request.getAuthorization();
DigestInfo digestInfo = new DigestInfo(getOpaque(), getNonceValidity(), getKey(), isValidateUri());
if (authorization != null) {
boolean validRequest = digestInfo.validate(httpServletRequest, authorization, config);
if (validRequest) {
principal = context.getRealm().authenticate(httpServletRequest);
if (principal != null) {
register(request, response, principal, DIGEST_METHOD, parseUsername(authorization), null);
String ssoId = (String) request.getNote(REQ_SSOID_NOTE);
if (ssoId != null) {
getSession(request, true);
}
return true;
}
}
}
// Send an "unauthorized" response and an appropriate challenge
// Next, generate a nOnce token (that is a token which is supposed
// to be unique).
setAuthenticateHeader(httpServletRequest, httpServletResponse, config, generateNonce(httpServletRequest), digestInfo.isNonceStale());
httpServletResponse.sendError(SC_UNAUTHORIZED);
return false;
}
// ------------------------------------------------------------- Properties
/**
* Return the message digest algorithm for this Manager.
*/
public static synchronized String getAlgorithm() {
return algorithm;
}
/**
* Set the message digest algorithm for this Manager.
*
* @param alg The new message digest algorithm
*/
public static synchronized void setAlgorithm(String alg) {
algorithm = alg;
// reset the messageDigest
messageDigest = null;
}
/**
* Return descriptive information about this Valve implementation.
*/
@Override
public String getInfo() {
return info;
}
public int getCnonceCacheSize() {
return cnonceCacheSize;
}
public void setCnonceCacheSize(int cnonceCacheSize) {
this.cnonceCacheSize = cnonceCacheSize;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public long getNonceValidity() {
return nonceValidity;
}
public void setNonceValidity(long nonceValidity) {
this.nonceValidity = nonceValidity;
}
public String getOpaque() {
return opaque;
}
public void setOpaque(String opaque) {
this.opaque = opaque;
}
public boolean isValidateUri() {
return validateUri;
}
public void setValidateUri(boolean validateUri) {
this.validateUri = validateUri;
}
// ------------------------------------------------------ Protected Methods
/**
* Parse the username from the specified authorization string. If none can be identified, return null
*
* @param authorization Authorization string to be parsed
*/
protected String parseUsername(String authorization) {
// Validate the authorization credentials format
if (authorization == null) {
return null;
}
if (!authorization.startsWith("Digest ")) {
return null;
}
authorization = authorization.substring(7).trim();
StringTokenizer commaTokenizer = new StringTokenizer(authorization, ",");
while (commaTokenizer.hasMoreTokens()) {
String currentToken = commaTokenizer.nextToken();
int equalSign = currentToken.indexOf('=');
if (equalSign < 0) {
return null;
}
String currentTokenName = currentToken.substring(0, equalSign).trim();
String currentTokenValue = currentToken.substring(equalSign + 1).trim();
if ("username".equals(currentTokenName)) {
return removeQuotes(currentTokenValue);
}
}
return null;
}
@Override
protected String getAuthMethod() {
return HttpServletRequest.DIGEST_AUTH;
}
/**
* Removes the quotes on a string.
*/
protected static String removeQuotes(String quotedString, boolean quotesRequired) {
// Support both quoted and non-quoted
if (quotedString.length() > 0 && quotedString.charAt(0) != '"' && !quotesRequired) {
return quotedString;
}
if (quotedString.length() > 2) {
return quotedString.substring(1, quotedString.length() - 1);
}
return EMPTY_STRING;
}
/**
* Removes the quotes on a string.
*/
protected static String removeQuotes(String quotedString) {
return removeQuotes(quotedString, false);
}
/**
* Generate a unique token. The token is generated according to the following pattern. NOnceToken = Base64 ( MD5 (
* client-IP ":" time-stamp ":" private-key ) ).
*
* @param request HTTP Servlet request
*/
protected String generateNonce(HttpServletRequest request) {
long currentTime = System.currentTimeMillis();
String ipTimeKey = request.getRemoteAddr() + ":" + currentTime + ":" + getKey();
byte[] buffer = digest(ipTimeKey.getBytes(Charset.defaultCharset()));
return currentTime + ":" + new String(digestEncoder.encode(buffer));
}
/**
* Generates the WWW-Authenticate header.
*
* The header MUST follow this template :
*
* {@code
* WWW-Authenticate = "WWW-Authenticate" ":" "Digest"
* digest-challenge
*
* digest-challenge = 1#( realm | [ domain ] | nOnce |
* [ digest-opaque ] |[ stale ] | [ algorithm ] )
*
* realm = "realm" "=" realm-value
* realm-value = quoted-string
* domain = "domain" "=" <"> 1#URI <">
* nonce = "nonce" "=" nonce-value
* nonce-value = quoted-string
* opaque = "opaque" "=" quoted-string
* stale = "stale" "=" ( "true" | "false" )
* algorithm = "algorithm" "=" ( "MD5" | token )
* }
*
* @param request HTTP Servlet request
* @param response HTTP Servlet response
* @param config Login configuration describing how authentication should be performed
* @param nOnce nonce token
*/
protected void setAuthenticateHeader(HttpServletRequest request, HttpServletResponse response, LoginConfig config, String nOnce,
boolean isNonceStale) {
// Get the realm name
String realmName = config.getRealmName();
if (realmName == null) {
realmName = REALM_NAME;
}
String authenticateHeader;
if (isNonceStale) {
authenticateHeader = "Digest realm=\"" + realmName + "\", " + "qop=\"" + QOP + "\", nonce=\"" + nOnce + "\", " + "opaque=\""
+ getOpaque() + "\", stale=true";
} else {
authenticateHeader = "Digest realm=\"" + realmName + "\", " + "qop=\"" + QOP + "\", nonce=\"" + nOnce + "\", " + "opaque=\""
+ getOpaque() + "\"";
}
response.setHeader(AUTH_HEADER_NAME, authenticateHeader);
}
protected static synchronized MessageDigest getMessageDigest() {
if (messageDigest == null) {
try {
messageDigest = MessageDigest.getInstance(algorithm);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(algorithm + " digest algorithm not available", e);
}
}
return messageDigest;
}
protected static byte[] digest(byte[] data) {
byte[] buffer = null;
MessageDigest md = getMessageDigest();
synchronized (md) {
buffer = md.digest(data);
}
return buffer;
}
// ------------------------------------------------------- Lifecycle Methods
@Override
public synchronized void start() throws LifecycleException {
super.start();
// Generate a random secret key
if (getKey() == null) {
setKey(generateSessionId());
}
// Generate the opaque string the same way
if (getOpaque() == null) {
setOpaque(generateSessionId());
}
}
private static class DigestInfo {
private String opaque;
private long nonceValidity;
private String key;
private boolean validateUri = true;
private String userName;
private String uri;
private String response;
private String nonce;
private String nc;
private String cnonce;
private String realmName;
private String qop;
private boolean nonceStale;
public DigestInfo(String opaque, long nonceValidity, String key, boolean validateUri) {
this.opaque = opaque;
this.nonceValidity = nonceValidity;
this.key = key;
this.validateUri = validateUri;
}
public boolean validate(HttpServletRequest request, String authorization, LoginConfig config) {
// Validate the authorization credentials format
if (authorization == null) {
return false;
}
if (!authorization.startsWith("Digest ")) {
return false;
}
authorization = authorization.substring(7).trim();
// Bugzilla 37132: http://issues.apache.org/bugzilla/show_bug.cgi?id=37132
String[] tokens = authorization.split(",(?=(?:[^\"]*\"[^\"]*\")+$)");
String opaque_client = null;
for (String currentToken : tokens) {
if (currentToken.length() == 0)
continue;
int equalSign = currentToken.indexOf('=');
if (equalSign < 0) {
return false;
}
String currentTokenName = currentToken.substring(0, equalSign).trim();
String currentTokenValue = currentToken.substring(equalSign + 1).trim();
if ("username".equals(currentTokenName))
userName = removeQuotes(currentTokenValue);
if ("realm".equals(currentTokenName))
realmName = removeQuotes(currentTokenValue, true);
if ("nonce".equals(currentTokenName))
nonce = removeQuotes(currentTokenValue);
if ("nc".equals(currentTokenName))
nc = removeQuotes(currentTokenValue);
if ("cnonce".equals(currentTokenName))
cnonce = removeQuotes(currentTokenValue);
if ("qop".equals(currentTokenName))
qop = removeQuotes(currentTokenValue);
if ("uri".equals(currentTokenName))
uri = removeQuotes(currentTokenValue);
if ("response".equals(currentTokenName))
response = removeQuotes(currentTokenValue);
if ("opaque".equals(currentTokenName))
opaque_client = removeQuotes(currentTokenValue);
}
if ((userName == null) || (realmName == null) || (nonce == null) || (uri == null) || (response == null)) {
return false;
}
// Validate the URI - should match the request line sent by client
if (validateUri) {
String uriQuery;
String query = request.getQueryString();
if (query == null) {
uriQuery = request.getRequestURI();
} else {
uriQuery = request.getRequestURI() + "?" + query;
}
if (!uri.equals(uriQuery)) {
return false;
}
}
// Validate the Realm name
String lcRealm = config.getRealmName();
if (lcRealm == null) {
lcRealm = REALM_NAME;
}
if (!lcRealm.equals(realmName)) {
return false;
}
// Validate the opaque string
if (!this.opaque.equals(opaque_client)) {
return false;
}
// Validate nonce
int i = nonce.indexOf(':');
if (i < 0 || (i + 1) == nonce.length()) {
return false;
}
long nOnceTime;
try {
nOnceTime = Long.parseLong(nonce.substring(0, i));
} catch (NumberFormatException nfe) {
return false;
}
String md5clientIpTimeKey = nonce.substring(i + 1);
long currentTime = System.currentTimeMillis();
if ((currentTime - nOnceTime) > nonceValidity) {
nonceStale = true;
return false;
}
String serverIpTimeKey = request.getRemoteAddr() + ":" + nOnceTime + ":" + key;
byte[] buffer = digest(serverIpTimeKey.getBytes(Charset.defaultCharset()));
String md5ServerIpTimeKey = new String(digestEncoder.encode(buffer));
if (!md5ServerIpTimeKey.equals(md5clientIpTimeKey)) {
return false;
}
// Validate qop
if (qop != null && !QOP.equals(qop)) {
return false;
}
// Validate cnonce and nc
// Check if presence of nc and nonce is consistent with presence of qop
if (qop == null) {
if (cnonce != null || nc != null) {
return false;
}
} else {
if (cnonce == null || nc == null) {
return false;
}
if (nc.length() != 8) {
return false;
}
try {
Long.parseLong(nc, 16);
} catch (NumberFormatException nfe) {
return false;
}
}
return true;
}
public boolean isNonceStale() {
return nonceStale;
}
}
}