SEC-2601: Add DigestRequestPostProcessor
This commit is contained in:
parent
c8348d60e1
commit
9654817fd8
|
@ -53,6 +53,7 @@ import org.springframework.security.web.csrf.CsrfTokenRepository;
|
|||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.request.RequestPostProcessor;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.DigestUtils;
|
||||
|
||||
/**
|
||||
* Contains {@link MockMvc} {@link RequestPostProcessor} implementations for
|
||||
|
@ -63,6 +64,25 @@ import org.springframework.util.Assert;
|
|||
*/
|
||||
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.
|
||||
* @param certificates the X509Certificate instances to pouplate
|
||||
|
@ -255,6 +275,134 @@ public final class SecurityMockMvcRequestPostProcessors {
|
|||
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
|
||||
* Security context
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue