Prechádzať zdrojové kódy

SEC-2601: Add DigestRequestPostProcessor

Rob Winch 11 rokov pred
rodič
commit
9654817fd8

+ 148 - 0
test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java

@@ -53,6 +53,7 @@ import org.springframework.security.web.csrf.CsrfTokenRepository;
 import org.springframework.test.web.servlet.MockMvc;
 import org.springframework.test.web.servlet.MockMvc;
 import org.springframework.test.web.servlet.request.RequestPostProcessor;
 import org.springframework.test.web.servlet.request.RequestPostProcessor;
 import org.springframework.util.Assert;
 import org.springframework.util.Assert;
+import org.springframework.util.DigestUtils;
 
 
 /**
 /**
  * Contains {@link MockMvc} {@link RequestPostProcessor} implementations for
  * Contains {@link MockMvc} {@link RequestPostProcessor} implementations for
@@ -63,6 +64,25 @@ import org.springframework.util.Assert;
  */
  */
 public final class SecurityMockMvcRequestPostProcessors {
 public final class SecurityMockMvcRequestPostProcessors {
 
 
+    /**
+     * Creates a DigestRequestPostProcessor that enables easily adding digest based authentication to a request.
+     *
+     * @return the DigestRequestPostProcessor to use
+     */
+    public static DigestRequestPostProcessor digest() {
+        return new DigestRequestPostProcessor();
+    }
+
+    /**
+     * Creates a DigestRequestPostProcessor that enables easily adding digest based authentication to a request.
+     *
+     * @param username the username to use
+     * @return the DigestRequestPostProcessor to use
+     */
+    public static DigestRequestPostProcessor digest(String username) {
+        return digest().username(username);
+    }
+
     /**
     /**
      * Populates the provided X509Certificate instances on the request.
      * Populates the provided X509Certificate instances on the request.
      * @param certificates the X509Certificate instances to pouplate
      * @param certificates the X509Certificate instances to pouplate
@@ -255,6 +275,134 @@ public final class SecurityMockMvcRequestPostProcessors {
         private CsrfRequestPostProcessor() {}
         private CsrfRequestPostProcessor() {}
     }
     }
 
 
+    public static class DigestRequestPostProcessor implements RequestPostProcessor {
+        private String username = "user";
+
+        private String password = "password";
+
+        private String realm = "Spring Security";
+
+        private String nonce = generateNonce(60);
+
+        private String qop = "auth";
+
+        private String nc = "00000001";
+
+        private String cnonce = "c822c727a648aba7";
+
+        /**
+         * Configures the username to use
+         * @param username the username to use
+         * @return the DigestRequestPostProcessor for further customization
+         */
+        private DigestRequestPostProcessor username(String username) {
+            Assert.notNull(username, "username cannot be null");
+            this.username = username;
+            return this;
+        }
+
+        /**
+         * Configures the password to use
+         * @param password the password to use
+         * @return the DigestRequestPostProcessor for further customization
+         */
+        public DigestRequestPostProcessor password(String password) {
+            Assert.notNull(password, "password cannot be null");
+            this.password = password;
+            return this;
+        }
+
+        /**
+         * Configures the realm to use
+         * @param realm the realm to use
+         * @return the DigestRequestPostProcessor for further customization
+         */
+        public DigestRequestPostProcessor realm(String realm) {
+            Assert.notNull(realm, "realm cannot be null");
+            this.realm = realm;
+            return this;
+        }
+
+        private static String generateNonce(int validitySeconds) {
+            long expiryTime = System.currentTimeMillis() + (validitySeconds * 1000);
+            String toDigest = expiryTime + ":" + "key";
+            String signatureValue = md5Hex(toDigest);
+            String nonceValue = expiryTime + ":" + signatureValue;
+
+            return new String(Base64.encode(nonceValue.getBytes()));
+        }
+
+        private String createAuthorizationHeader(MockHttpServletRequest request) {
+            String uri = request.getRequestURI();
+            String responseDigest = generateDigest(username, realm, password, request.getMethod(),
+                    uri, qop, nonce, nc, cnonce);
+            return "Digest username=\"" + username + "\", realm=\"" + realm + "\", nonce=\"" + nonce + "\", uri=\"" + uri
+                    + "\", response=\"" + responseDigest + "\", qop=" + qop + ", nc=" + nc + ", cnonce=\"" + cnonce + "\"";
+        }
+
+        @Override
+        public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
+
+            request.addHeader("Authorization",
+                    createAuthorizationHeader(request));
+            return request;
+        }
+
+
+        /**
+         * Computes the <code>response</code> portion of a Digest authentication header. Both the server and user
+         * agent should compute the <code>response</code> independently. Provided as a static method to simplify the
+         * coding of user agents.
+         *
+         * @param username               the user's login name.
+         * @param realm                  the name of the realm.
+         * @param password               the user's password in plaintext or ready-encoded.
+         * @param httpMethod             the HTTP request method (GET, POST etc.)
+         * @param uri                    the request URI.
+         * @param qop                    the qop directive, or null if not set.
+         * @param nonce                  the nonce supplied by the server
+         * @param nc                     the "nonce-count" as defined in RFC 2617.
+         * @param cnonce                 opaque string supplied by the client when qop is set.
+         * @return the MD5 of the digest authentication response, encoded in hex
+         * @throws IllegalArgumentException if the supplied qop value is unsupported.
+         */
+        private static String generateDigest(String username, String realm, String password,
+                                     String httpMethod, String uri, String qop, String nonce, String nc, String cnonce)
+                throws IllegalArgumentException {
+            String a1Md5 = encodePasswordInA1Format(username, realm, password);
+            String a2 = httpMethod + ":" + uri;
+            String a2Md5 = md5Hex(a2);
+
+            String digest;
+
+            if (qop == null) {
+                // as per RFC 2069 compliant clients (also reaffirmed by RFC 2617)
+                digest = a1Md5 + ":" + nonce + ":" + a2Md5;
+            } else if ("auth".equals(qop)) {
+                // As per RFC 2617 compliant clients
+                digest = a1Md5 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + a2Md5;
+            } else {
+                throw new IllegalArgumentException("This method does not support a qop: '" + qop + "'");
+            }
+
+            return md5Hex(digest);
+        }
+
+        static String encodePasswordInA1Format(String username, String realm, String password) {
+            String a1 = username + ":" + realm + ":" + password;
+
+            return md5Hex(a1);
+        }
+
+        private static String md5Hex(String a2) {
+            try {
+                return DigestUtils.md5DigestAsHex(a2.getBytes("UTF-8"));
+            } catch(UnsupportedEncodingException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
     /**
     /**
      * Support class for {@link RequestPostProcessor}'s that establish a Spring
      * Support class for {@link RequestPostProcessor}'s that establish a Spring
      * Security context
      * Security context

+ 128 - 0
test/src/test/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessorsDigestTests.java

@@ -0,0 +1,128 @@
+/*
+ * Copyright 2002-2014 the original author or authors.
+ *
+ * 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.
+ */
+package org.springframework.security.test.web.servlet.request;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.mock.web.MockFilterChain;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.web.authentication.www.DigestAuthenticationEntryPoint;
+import org.springframework.security.web.authentication.www.DigestAuthenticationFilter;
+
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import java.io.IOException;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.digest;
+
+public class SecurityMockMvcRequestPostProcessorsDigestTests {
+
+    private DigestAuthenticationFilter filter;
+    private MockHttpServletRequest request;
+
+    private String username;
+
+    private String password;
+
+    private DigestAuthenticationEntryPoint entryPoint;
+
+    @Before
+    public void setup() {
+        this.password = "password";
+        request = new MockHttpServletRequest();
+
+        entryPoint = new DigestAuthenticationEntryPoint();
+        entryPoint.setKey("key");
+        entryPoint.setRealmName("Spring Security");
+        filter = new DigestAuthenticationFilter();
+        filter.setUserDetailsService(new UserDetailsService() {
+            @Override
+            public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+                return new User(username,password, AuthorityUtils.createAuthorityList("ROLE_USER"));
+            }
+        });
+        filter.setAuthenticationEntryPoint(entryPoint);
+        filter.afterPropertiesSet();
+    }
+
+    @After
+    public void cleanup() {
+        SecurityContextHolder.clearContext();
+    }
+
+    @Test
+    public void digestWithFilter() throws Exception  {
+        MockHttpServletRequest postProcessedRequest = digest().postProcessRequest(request);
+
+        assertThat(extractUser()).isEqualTo("user");
+    }
+
+    @Test
+    public void digestWithFilterCustomUsername() throws Exception  {
+        String username = "admin";
+        MockHttpServletRequest postProcessedRequest = digest(username).postProcessRequest(request);
+
+        assertThat(extractUser()).isEqualTo(username);
+    }
+
+    @Test
+    public void digestWithFilterCustomPassword() throws Exception  {
+        String username = "custom";
+        password = "secret";
+        MockHttpServletRequest postProcessedRequest = digest(username).password(password).postProcessRequest(request);
+
+        assertThat(extractUser()).isEqualTo(username);
+    }
+
+    @Test
+    public void digestWithFilterCustomRealm() throws Exception  {
+        String username = "admin";
+        entryPoint.setRealmName("Custom");
+        MockHttpServletRequest postProcessedRequest = digest(username).realm(entryPoint.getRealmName()).postProcessRequest(request);
+
+        assertThat(extractUser()).isEqualTo(username);
+    }
+
+    @Test
+    public void digestWithFilterFails() throws Exception  {
+        String username = "admin";
+        MockHttpServletRequest postProcessedRequest = digest(username).realm("Invalid").postProcessRequest(request);
+
+        assertThat(extractUser()).isNull();
+    }
+
+    private String extractUser() throws IOException, ServletException {
+        filter.doFilter(request, new MockHttpServletResponse(), new MockFilterChain() {
+            @Override
+            public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
+                Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+                username = authentication == null ? null : authentication.getName();
+            }
+        });
+        return username;
+    }
+}