All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.catalina.authenticator.DigestAuthenticator Maven / Gradle / Ivy

There is a newer version: 7.2024.1.Alpha1
Show newest version
/*
 * 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; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy