You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by ma...@apache.org on 2015/06/23 12:03:20 UTC

svn commit: r1687016 - in /tomcat/trunk/java/org/apache/catalina/authenticator/jaspic/provider: TomcatAuthConfig.java modules/DigestAuthModule.java

Author: markt
Date: Tue Jun 23 10:03:20 2015
New Revision: 1687016

URL: http://svn.apache.org/r1687016
Log:
Implemented JASPIC module for DIGEST authentication
Patch by fjodorver

Added:
    tomcat/trunk/java/org/apache/catalina/authenticator/jaspic/provider/modules/DigestAuthModule.java   (with props)
Modified:
    tomcat/trunk/java/org/apache/catalina/authenticator/jaspic/provider/TomcatAuthConfig.java

Modified: tomcat/trunk/java/org/apache/catalina/authenticator/jaspic/provider/TomcatAuthConfig.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/authenticator/jaspic/provider/TomcatAuthConfig.java?rev=1687016&r1=1687015&r2=1687016&view=diff
==============================================================================
--- tomcat/trunk/java/org/apache/catalina/authenticator/jaspic/provider/TomcatAuthConfig.java (original)
+++ tomcat/trunk/java/org/apache/catalina/authenticator/jaspic/provider/TomcatAuthConfig.java Tue Jun 23 10:03:20 2015
@@ -30,6 +30,7 @@ import javax.security.auth.message.confi
 
 import org.apache.catalina.Realm;
 import org.apache.catalina.authenticator.jaspic.provider.modules.BasicAuthModule;
+import org.apache.catalina.authenticator.jaspic.provider.modules.DigestAuthModule;
 import org.apache.catalina.authenticator.jaspic.provider.modules.TomcatAuthModule;
 
 public class TomcatAuthConfig implements ServerAuthConfig {
@@ -94,6 +95,7 @@ public class TomcatAuthConfig implements
     private Collection<TomcatAuthModule> getModules() {
         List<TomcatAuthModule> modules = new ArrayList<>();
         modules.add(new BasicAuthModule());
+        modules.add(new DigestAuthModule(realm));
         return modules;
     }
 }

Added: tomcat/trunk/java/org/apache/catalina/authenticator/jaspic/provider/modules/DigestAuthModule.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/authenticator/jaspic/provider/modules/DigestAuthModule.java?rev=1687016&view=auto
==============================================================================
--- tomcat/trunk/java/org/apache/catalina/authenticator/jaspic/provider/modules/DigestAuthModule.java (added)
+++ tomcat/trunk/java/org/apache/catalina/authenticator/jaspic/provider/modules/DigestAuthModule.java Tue Jun 23 10:03:20 2015
@@ -0,0 +1,647 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.catalina.authenticator.jaspic.provider.modules;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.nio.charset.StandardCharsets;
+import java.security.Principal;
+import java.text.MessageFormat;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.message.AuthException;
+import javax.security.auth.message.AuthStatus;
+import javax.security.auth.message.MessageInfo;
+import javax.security.auth.message.MessagePolicy;
+import javax.security.auth.message.callback.CallerPrincipalCallback;
+import javax.security.auth.message.callback.GroupPrincipalCallback;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.catalina.Realm;
+import org.apache.catalina.realm.GenericPrincipal;
+import org.apache.catalina.util.StandardSessionIdGenerator;
+import org.apache.juli.logging.Log;
+import org.apache.juli.logging.LogFactory;
+import org.apache.tomcat.util.http.parser.Authorization;
+import org.apache.tomcat.util.security.ConcurrentMessageDigest;
+import org.apache.tomcat.util.security.MD5Encoder;
+
+public class DigestAuthModule extends TomcatAuthModule {
+    private static final Log log = LogFactory.getLog(DigestAuthModule.class);
+    /**
+     * Tomcat's DIGEST implementation only supports auth quality of protection.
+     */
+    protected static final String QOP = "auth";
+
+    private Class<?>[] supportedMessageTypes = new Class[] { HttpServletRequest.class,
+            HttpServletResponse.class };
+
+    private CallbackHandler handler;
+
+    private Realm realm;
+
+    /**
+     * List of server nonce values currently being tracked
+     */
+    protected Map<String, NonceInfo> nonces;
+
+    /**
+     * The last timestamp used to generate a nonce. Each nonce should get a
+     * unique timestamp.
+     */
+    protected long lastTimestamp = 0;
+    protected final Object lastTimestampLock = new Object();
+
+    /**
+     * Maximum number of server nonces to keep in the cache. If not specified,
+     * the default value of 1000 is used.
+     */
+    protected int nonceCacheSize = 1000;
+
+    /**
+     * The window size to use to track seen nonce count values for a given
+     * nonce. If not specified, the default of 100 is used.
+     */
+    protected int nonceCountWindowSize = 100;
+
+    /**
+     * Private key.
+     */
+    protected String key = null;
+
+    /**
+     * 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;
+    private StandardSessionIdGenerator sessionIdGenerator;
+
+
+    // ------------------------------------------------------------- Properties
+
+    public DigestAuthModule(Realm realm) {
+        this.realm = realm;
+    }
+
+
+    public int getNonceCountWindowSize() {
+        return nonceCountWindowSize;
+    }
+
+
+    public void setNonceCountWindowSize(int nonceCountWindowSize) {
+        this.nonceCountWindowSize = nonceCountWindowSize;
+    }
+
+
+    public int getNonceCacheSize() {
+        return nonceCacheSize;
+    }
+
+
+    public void setNonceCacheSize(int nonceCacheSize) {
+        this.nonceCacheSize = nonceCacheSize;
+    }
+
+
+    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;
+    }
+
+
+    public void setRealm(Realm realm) {
+        this.realm = realm;
+    }
+
+
+    @Override
+    public String getAuthenticationType() {
+        return "DIGEST";
+    }
+
+
+    @SuppressWarnings("rawtypes")
+    @Override
+    public void initialize(MessagePolicy requestPolicy, MessagePolicy responsePolicy,
+            CallbackHandler handler, Map options) throws AuthException {
+        this.handler = handler;
+        startInternal();
+    }
+
+
+    protected synchronized void startInternal() {
+        this.sessionIdGenerator = new StandardSessionIdGenerator();
+
+        // Generate a random secret key
+        if (getKey() == null) {
+            setKey(sessionIdGenerator.generateSessionId());
+        }
+
+        // Generate the opaque string the same way
+        if (getOpaque() == null) {
+            setOpaque(sessionIdGenerator.generateSessionId());
+        }
+
+        nonces = new LinkedHashMap<String, NonceInfo>() {
+
+            private static final long serialVersionUID = 1L;
+            private static final long LOG_SUPPRESS_TIME = 5 * 60 * 1000;
+
+            private long lastLog = 0;
+
+            @Override
+            protected boolean removeEldestEntry(Map.Entry<String, NonceInfo> eldest) {
+                // This is called from a sync so keep it simple
+                long currentTime = System.currentTimeMillis();
+                if (size() > getNonceCacheSize()) {
+                    if (lastLog < currentTime
+                            && currentTime - eldest.getValue().getTimestamp() < getNonceValidity()) {
+                        // Replay attack is possible
+                        log.warn(sm.getString("digestAuthenticator.cacheRemove"));
+                        lastLog = currentTime + LOG_SUPPRESS_TIME;
+                    }
+                    return true;
+                }
+                return false;
+            }
+        };
+    }
+
+
+    @Override
+    public AuthStatus validateRequest(MessageInfo messageInfo, Subject clientSubject,
+            Subject serviceSubject) throws AuthException {
+
+        GenericPrincipal principal = null;
+        HttpServletRequest request = (HttpServletRequest) messageInfo.getRequestMessage();
+        HttpServletResponse response = (HttpServletResponse) messageInfo.getResponseMessage();
+        String authorization = request.getHeader(AUTHORIZATION_HEADER);
+
+        DigestInfo digestInfo = new DigestInfo(getOpaque(), getNonceValidity(), getKey(), nonces,
+                isValidateUri());
+        if (authorization == null) {
+
+            String nonce = generateNonce(request);
+
+            String authenticateHeader = getAuthenticateHeader(nonce, false, messageInfo);
+            return sendUnauthorizedError(response, authenticateHeader);
+        }
+
+        if (!digestInfo.parse(request, authorization)) {
+            return AuthStatus.SEND_FAILURE;
+        }
+
+        if (digestInfo.validate(request, messageInfo)) {
+            // TODO discuss a better way to get user roles
+            principal = (GenericPrincipal) digestInfo.authenticate(realm);
+        }
+
+        if (principal == null || digestInfo.isNonceStale()) {
+            String nonce = generateNonce(request);
+            boolean isNoncaneStale = principal != null && digestInfo.isNonceStale();
+            String authenticateHeader = getAuthenticateHeader(nonce, isNoncaneStale, messageInfo);
+            return sendUnauthorizedError(response, authenticateHeader);
+        }
+
+        try {
+            CallerPrincipalCallback principalCallback = new CallerPrincipalCallback(clientSubject,
+                    principal);
+            GroupPrincipalCallback groupCallback = new GroupPrincipalCallback(clientSubject,
+                    principal.getRoles());
+            handler.handle(new Callback[] { principalCallback, groupCallback });
+        } catch (IOException | UnsupportedCallbackException e) {
+            throw new AuthException(e.getMessage());
+        }
+        return AuthStatus.SUCCESS;
+    }
+
+
+    private AuthStatus sendUnauthorizedError(HttpServletResponse response, String authenticateHeader)
+            throws AuthException {
+        response.setHeader(AUTH_HEADER_NAME, authenticateHeader);
+        try {
+            response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+        } catch (IOException e) {
+            throw new AuthException(e.getMessage());
+        }
+        return AuthStatus.SEND_CONTINUE;
+    }
+
+
+    @Override
+    public AuthStatus secureResponse(MessageInfo messageInfo, Subject serviceSubject)
+            throws AuthException {
+        return null;
+    }
+
+
+    @Override
+    public void cleanSubject(MessageInfo messageInfo, Subject subject) throws AuthException {
+
+    }
+
+
+    @Override
+    public Class<?>[] getSupportedMessageTypes() {
+        return supportedMessageTypes;
+    }
+
+
+    /**
+     * Removes the quotes on a string. RFC2617 states quotes are optional for
+     * all parameters except realm.
+     */
+    protected static String removeQuotes(String quotedString, boolean quotesRequired) {
+        // support both quoted and non-quoted
+        if (quotedString.length() > 0 && quotedString.charAt(0) != '"' && !quotesRequired) {
+            return quotedString;
+        } else if (quotedString.length() > 2) {
+            return quotedString.substring(1, quotedString.length() - 1);
+        } else {
+            return "";
+        }
+    }
+
+
+    /**
+     * 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();
+
+        synchronized (lastTimestampLock) {
+            if (currentTime > lastTimestamp) {
+                lastTimestamp = currentTime;
+            } else {
+                currentTime = ++lastTimestamp;
+            }
+        }
+
+        String ipTimeKey = request.getRemoteAddr() + ":" + currentTime + ":" + getKey();
+
+        byte[] buffer = ConcurrentMessageDigest.digestMD5(ipTimeKey
+                .getBytes(StandardCharsets.ISO_8859_1));
+        String nonce = currentTime + ":" + MD5Encoder.encode(buffer);
+
+        NonceInfo info = new NonceInfo(currentTime, getNonceCountWindowSize());
+        synchronized (nonces) {
+            nonces.put(nonce, info);
+        }
+
+        return nonce;
+    }
+
+
+    /**
+     * Generates the WWW-Authenticate header.
+     * <p>
+     * The header MUST follow this template :
+     *
+     * <pre>
+     *      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" "=" &lt;"&gt; 1#URI &lt;"&gt;
+     *      nonce               = "nonce" "=" nonce-value
+     *      nonce-value         = quoted-string
+     *      opaque              = "opaque" "=" quoted-string
+     *      stale               = "stale" "=" ( "true" | "false" )
+     *      algorithm           = "algorithm" "=" ( "MD5" | token )
+     * </pre>
+     *
+     * @param nonce nonce token
+     * @return
+     */
+    protected String getAuthenticateHeader(String nonce, boolean isNonceStale,
+            MessageInfo messageInfo) {
+
+        String realmName = getRealmName(messageInfo);
+
+        String template = "Digest realm=\"{0}\", qop=\"{1}\", nonce=\"{2}\", opaque=\"{3}\"";
+        String authenticateHeader = MessageFormat.format(template, realmName, QOP, nonce,
+                getOpaque());
+        if (!isNonceStale) {
+            return authenticateHeader;
+        }
+        return authenticateHeader + ", stale=true";
+    }
+
+
+    private static class DigestInfo {
+
+        private final String opaque;
+        private final long nonceValidity;
+        private final String key;
+        private final Map<String, NonceInfo> nonces;
+        private boolean validateUri = true;
+
+        private String userName = null;
+        private String method = null;
+        private String uri = null;
+        private String response = null;
+        private String nonce = null;
+        private String nc = null;
+        private String cnonce = null;
+        private String realmName = null;
+        private String qop = null;
+        private String opaqueReceived = null;
+
+        private boolean nonceStale = false;
+
+        public DigestInfo(String opaque, long nonceValidity, String key,
+                Map<String, NonceInfo> nonces, boolean validateUri) {
+            this.opaque = opaque;
+            this.nonceValidity = nonceValidity;
+            this.key = key;
+            this.nonces = nonces;
+            this.validateUri = validateUri;
+        }
+
+        public String getUsername() {
+            return userName;
+        }
+
+        public boolean parse(HttpServletRequest request, String authorization) {
+            // Validate the authorization credentials format
+            if (authorization == null) {
+                return false;
+            }
+
+            Map<String, String> directives;
+            try {
+                directives = Authorization.parseAuthorizationDigest(
+                        new StringReader(authorization));
+            } catch (IOException e) {
+                return false;
+            }
+
+            if (directives == null) {
+                return false;
+            }
+
+            method = request.getMethod();
+            userName = directives.get("username");
+            realmName = directives.get("realm");
+            nonce = directives.get("nonce");
+            nc = directives.get("nc");
+            cnonce = directives.get("cnonce");
+            qop = directives.get("qop");
+            uri = directives.get("uri");
+            response = directives.get("response");
+            opaqueReceived = directives.get("opaque");
+
+            return true;
+        }
+
+        public boolean validate(HttpServletRequest request, MessageInfo messageInfo) {
+            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)) {
+                    // Some clients (older Android) use an absolute URI for
+                    // DIGEST but a relative URI in the request line.
+                    // request. 2.3.5 < fixed Android version <= 4.0.3
+                    String host = request.getHeader("host");
+                    String scheme = request.getScheme();
+                    if (host != null && !uriQuery.startsWith(scheme)) {
+                        StringBuilder absolute = new StringBuilder();
+                        absolute.append(scheme);
+                        absolute.append("://");
+                        absolute.append(host);
+                        absolute.append(uriQuery);
+                        if (!uri.equals(absolute.toString())) {
+                            return false;
+                        }
+                    } else {
+                        return false;
+                    }
+                }
+            }
+
+            // Validate the Realm name
+            String lcRealm = getRealmName(messageInfo);
+            if (!lcRealm.equals(realmName)) {
+                return false;
+            }
+
+            // Validate the opaque string
+            if (!opaque.equals(opaqueReceived)) {
+                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;
+                synchronized (nonces) {
+                    nonces.remove(nonce);
+                }
+            }
+            String serverIpTimeKey = request.getRemoteAddr() + ":" + nonceTime + ":" + key;
+            byte[] buffer = ConcurrentMessageDigest.digestMD5(serverIpTimeKey
+                    .getBytes(StandardCharsets.ISO_8859_1));
+            String md5ServerIpTimeKey = MD5Encoder.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 Cnonce is consistent with presence of
+            // qop
+            if (qop == null) {
+                if (cnonce != null || nc != null) {
+                    return false;
+                }
+            } else {
+                if (cnonce == null || nc == null) {
+                    return false;
+                }
+                // RFC 2617 says nc must be 8 digits long. Older Android clients
+                // use 6. 2.3.5 < fixed Android version <= 4.0.3
+                if (nc.length() < 6 || nc.length() > 8) {
+                    return false;
+                }
+                long count;
+                try {
+                    count = Long.parseLong(nc, 16);
+                } catch (NumberFormatException nfe) {
+                    return false;
+                }
+                NonceInfo info;
+                synchronized (nonces) {
+                    info = nonces.get(nonce);
+                }
+                if (info == null) {
+                    // Nonce is valid but not in cache. It must have dropped out
+                    // of the cache - force a re-authentication
+                    nonceStale = true;
+                } else {
+                    if (!info.nonceCountValid(count)) {
+                        return false;
+                    }
+                }
+            }
+            return true;
+        }
+
+        public boolean isNonceStale() {
+            return nonceStale;
+        }
+
+        public Principal authenticate(Realm realm) {
+            // Second MD5 digest used to calculate the digest :
+            // MD5(Method + ":" + uri)
+            String a2 = method + ":" + uri;
+
+            byte[] buffer = ConcurrentMessageDigest.digestMD5(a2
+                    .getBytes(StandardCharsets.ISO_8859_1));
+            String md5a2 = MD5Encoder.encode(buffer);
+
+            return realm.authenticate(userName, response, nonce, nc, cnonce, qop, realmName, md5a2);
+        }
+
+    }
+
+
+    private static class NonceInfo {
+        private final long timestamp;
+        private final boolean seen[];
+        private final int offset;
+        private int count = 0;
+
+        public NonceInfo(long currentTime, int seenWindowSize) {
+            this.timestamp = currentTime;
+            seen = new boolean[seenWindowSize];
+            offset = seenWindowSize / 2;
+        }
+
+        public synchronized boolean nonceCountValid(long nonceCount) {
+            if ((count - offset) >= nonceCount || (nonceCount > count - offset + seen.length)) {
+                return false;
+            }
+            int checkIndex = (int) ((nonceCount + offset) % seen.length);
+            if (seen[checkIndex]) {
+                return false;
+            } else {
+                seen[checkIndex] = true;
+                seen[count % seen.length] = false;
+                count++;
+                return true;
+            }
+        }
+
+        public long getTimestamp() {
+            return timestamp;
+        }
+    }
+}

Propchange: tomcat/trunk/java/org/apache/catalina/authenticator/jaspic/provider/modules/DigestAuthModule.java
------------------------------------------------------------------------------
    svn:eol-style = native



---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org