Skip to content

Commit 37ed413

Browse files
michaeltecourtDave Syer
authored andcommitted
Allow custom authentication filters for the TokenEndpoint.
These filters are added upstream of the traditional BasicAuthenticationFilter. Simple integration test project added, details in README.md. Fixes spring-atticgh-601, fixes spring-atticgh-602
1 parent b8603c0 commit 37ed413

File tree

9 files changed

+348
-1
lines changed

9 files changed

+348
-1
lines changed

spring-security-oauth2/src/main/java/org/springframework/security/oauth2/config/annotation/web/configurers/AuthorizationServerSecurityConfigurer.java

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
*/
1616
package org.springframework.security.oauth2.config.annotation.web.configurers;
1717

18+
import java.util.ArrayList;
1819
import java.util.Collections;
20+
import java.util.List;
21+
22+
import javax.servlet.Filter;
1923

2024
import org.springframework.http.MediaType;
2125
import org.springframework.security.authentication.AuthenticationManager;
@@ -36,6 +40,7 @@
3640
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
3741
import org.springframework.security.web.context.NullSecurityContextRepository;
3842
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
43+
import org.springframework.util.Assert;
3944
import org.springframework.util.StringUtils;
4045
import org.springframework.web.accept.ContentNegotiationStrategy;
4146
import org.springframework.web.accept.HeaderContentNegotiationStrategy;
@@ -65,6 +70,12 @@ public final class AuthorizationServerSecurityConfigurer extends
6570

6671
private boolean sslOnly = false;
6772

73+
/**
74+
* Custom authentication filters for the TokenEndpoint. Filters will be set upstream of the default
75+
* BasicAuthenticationFilter.
76+
*/
77+
private List<Filter> tokenEndpointAuthenticationFilters = new ArrayList<Filter>();
78+
6879
public AuthorizationServerSecurityConfigurer sslOnly() {
6980
this.sslOnly = true;
7081
return this;
@@ -172,8 +183,13 @@ public void configure(HttpSecurity http) throws Exception {
172183
if (allowFormAuthenticationForClients) {
173184
clientCredentialsTokenEndpointFilter(http);
174185
}
186+
187+
for (Filter filter : tokenEndpointAuthenticationFilters) {
188+
http.addFilterBefore(filter, BasicAuthenticationFilter.class);
189+
}
190+
175191
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
176-
if (sslOnly ) {
192+
if (sslOnly) {
177193
http.requiresChannel().anyRequest().requiresSecure();
178194
}
179195

@@ -201,4 +217,24 @@ private FrameworkEndpointHandlerMapping frameworkEndpointHandlerMapping() {
201217
return getBuilder().getSharedObject(FrameworkEndpointHandlerMapping.class);
202218
}
203219

220+
/**
221+
* Adds a new custom authentication filter for the TokenEndpoint. Filters will be set upstream of the default
222+
* BasicAuthenticationFilter.
223+
*
224+
* @param filter
225+
*/
226+
public void addTokenEndpointAuthenticationFilter(Filter filter) {
227+
this.tokenEndpointAuthenticationFilters.add(filter);
228+
}
229+
230+
/**
231+
* Sets a new list of custom authentication filters for the TokenEndpoint. Filters will be set upstream of the
232+
* default BasicAuthenticationFilter.
233+
*
234+
* @param filters The authentication filters to set.
235+
*/
236+
public void tokenEndpointAuthenticationFilters(List<Filter> filters) {
237+
Assert.notNull(filters, "Custom authentication filter list must not be null");
238+
this.tokenEndpointAuthenticationFilters = new ArrayList<Filter>(filters);
239+
}
204240
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
This project tests a basic authorization server configuration, with a custom authentication filter on the `TokenEndpoint`.
2+
The authentication mechanism only authorizes one fixed client to pass. The client_id is taken from an HTTP parameter.
3+
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
3+
<modelVersion>4.0.0</modelVersion>
4+
5+
<artifactId>spring-oauth2-tests-custom-authentication</artifactId>
6+
7+
<name>spring-oauth2-tests-custom-authentication</name>
8+
<description>Demo project</description>
9+
10+
<parent>
11+
<groupId>org.demo</groupId>
12+
<artifactId>spring-oauth2-tests-parent</artifactId>
13+
<version>2.0.8.RELEASE</version>
14+
</parent>
15+
16+
<dependencies>
17+
<dependency>
18+
<groupId>org.springframework.boot</groupId>
19+
<artifactId>spring-boot-starter-web</artifactId>
20+
</dependency>
21+
<dependency>
22+
<groupId>org.springframework.boot</groupId>
23+
<artifactId>spring-boot-starter-actuator</artifactId>
24+
</dependency>
25+
<dependency>
26+
<groupId>org.springframework.boot</groupId>
27+
<artifactId>spring-boot-starter-security</artifactId>
28+
</dependency>
29+
<dependency>
30+
<groupId>org.springframework.security.oauth</groupId>
31+
<artifactId>spring-security-oauth2</artifactId>
32+
</dependency>
33+
<dependency>
34+
<groupId>org.demo</groupId>
35+
<artifactId>spring-oauth2-tests-common</artifactId>
36+
<version>${project.version}</version>
37+
<scope>test</scope>
38+
</dependency>
39+
</dependencies>
40+
41+
<build>
42+
<plugins>
43+
<plugin>
44+
<groupId>org.springframework.boot</groupId>
45+
<artifactId>spring-boot-maven-plugin</artifactId>
46+
</plugin>
47+
</plugins>
48+
</build>
49+
50+
</project>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package demo;
2+
3+
import org.springframework.beans.factory.annotation.Autowired;
4+
import org.springframework.boot.SpringApplication;
5+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
6+
import org.springframework.context.annotation.Configuration;
7+
import org.springframework.http.HttpStatus;
8+
import org.springframework.security.authentication.AuthenticationManager;
9+
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
10+
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
11+
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
12+
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
13+
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
14+
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
15+
import org.springframework.util.MultiValueMap;
16+
import org.springframework.web.bind.annotation.RequestBody;
17+
import org.springframework.web.bind.annotation.RequestMapping;
18+
import org.springframework.web.bind.annotation.RequestMethod;
19+
import org.springframework.web.bind.annotation.ResponseStatus;
20+
import org.springframework.web.bind.annotation.RestController;
21+
22+
@Configuration
23+
@EnableAutoConfiguration
24+
@EnableResourceServer
25+
@RestController
26+
public class Application {
27+
28+
public static void main(String[] args) {
29+
SpringApplication.run(Application.class, args);
30+
}
31+
32+
@RequestMapping("/")
33+
public String home() {
34+
return "Hello World";
35+
}
36+
37+
@RequestMapping(value = "/", method = RequestMethod.POST)
38+
@ResponseStatus(HttpStatus.CREATED)
39+
public String create(@RequestBody MultiValueMap<String, String> map) {
40+
return "OK";
41+
}
42+
43+
@Configuration
44+
@EnableAuthorizationServer
45+
protected static class OAuth2Config extends AuthorizationServerConfigurerAdapter {
46+
47+
@Autowired
48+
private AuthenticationManager authenticationManager;
49+
50+
@Override
51+
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
52+
endpoints.authenticationManager(authenticationManager);
53+
}
54+
55+
@Override
56+
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
57+
// @formatter:off
58+
clients.inMemory().withClient("my-trusted-client")
59+
.authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit")
60+
.authorities("ROLE_CLIENT", "ROLE_TRUSTED_CLIENT").scopes("read", "write", "trust")
61+
.resourceIds("oauth2-resource").accessTokenValiditySeconds(600).and()
62+
.withClient("my-client-with-registered-redirect").authorizedGrantTypes("authorization_code")
63+
.authorities("ROLE_CLIENT").scopes("read", "trust").resourceIds("oauth2-resource")
64+
.redirectUris("http://anywhere?key=value").and().withClient("my-client-with-secret")
65+
.authorizedGrantTypes("client_credentials", "password").authorities("ROLE_CLIENT").scopes("read")
66+
.resourceIds("oauth2-resource").secret("secret");
67+
// @formatter:on
68+
}
69+
70+
@Override
71+
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
72+
security.addTokenEndpointAuthenticationFilter(new HardCodedAuthenticationFilter());
73+
}
74+
}
75+
76+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package demo;
2+
3+
import java.io.IOException;
4+
import java.util.List;
5+
6+
import javax.servlet.Filter;
7+
import javax.servlet.FilterChain;
8+
import javax.servlet.FilterConfig;
9+
import javax.servlet.ServletException;
10+
import javax.servlet.ServletRequest;
11+
import javax.servlet.ServletResponse;
12+
13+
import org.slf4j.Logger;
14+
import org.slf4j.LoggerFactory;
15+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
16+
import org.springframework.security.core.GrantedAuthority;
17+
import org.springframework.security.core.authority.AuthorityUtils;
18+
import org.springframework.security.core.context.SecurityContextHolder;
19+
import org.springframework.security.oauth2.common.util.OAuth2Utils;
20+
21+
/**
22+
* Authentication filter that would only authenticate one client, using the
23+
* "client_id" parameter.
24+
*
25+
* @author mtecourt
26+
*
27+
*/
28+
public class HardCodedAuthenticationFilter implements Filter {
29+
30+
private static final String AUTHORIZED_CLIENT_ID = "my-client-with-secret";
31+
private static final List<GrantedAuthority> CLIENT_AUTHORITIES = AuthorityUtils
32+
.commaSeparatedStringToAuthorityList("ROLE_CLIENT");
33+
34+
private static final Logger LOGGER = LoggerFactory.getLogger("CustomAuthenticationFilter");
35+
36+
@Override
37+
public void init(FilterConfig filterConfig) throws ServletException {
38+
// NOPE
39+
}
40+
41+
@Override
42+
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
43+
ServletException {
44+
45+
String clientId = request.getParameter(OAuth2Utils.CLIENT_ID);
46+
47+
if (AUTHORIZED_CLIENT_ID.equals(clientId)) {
48+
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
49+
AUTHORIZED_CLIENT_ID, "", CLIENT_AUTHORITIES);
50+
SecurityContextHolder.getContext().setAuthentication(authentication);
51+
LOGGER.info("Just authenticated : {}", clientId);
52+
} else {
53+
LOGGER.info("Did NOT authenticate : {}", clientId);
54+
}
55+
56+
chain.doFilter(request, response);
57+
}
58+
59+
@Override
60+
public void destroy() {
61+
// NOPE
62+
}
63+
64+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
spring:
2+
application:
3+
name: vanilla
4+
management:
5+
context_path: /admin
6+
security:
7+
user:
8+
password: password
9+
logging:
10+
level:
11+
# org.springframework.security: DEBUG
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package demo;
2+
3+
import org.junit.Test;
4+
import org.junit.runner.RunWith;
5+
import org.springframework.boot.test.IntegrationTest;
6+
import org.springframework.boot.test.SpringApplicationConfiguration;
7+
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
8+
import org.springframework.test.context.web.WebAppConfiguration;
9+
10+
@RunWith(SpringJUnit4ClassRunner.class)
11+
@SpringApplicationConfiguration(classes = Application.class)
12+
@WebAppConfiguration
13+
@IntegrationTest("server.port=0")
14+
public class ApplicationTests {
15+
16+
@Test
17+
public void contextLoads() {
18+
}
19+
20+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package demo;
2+
3+
import static org.junit.Assert.assertEquals;
4+
import static org.junit.Assert.assertNotNull;
5+
import static org.junit.Assert.fail;
6+
7+
import java.net.URI;
8+
import java.util.Map;
9+
10+
import org.junit.Before;
11+
import org.junit.Test;
12+
import org.springframework.boot.test.SpringApplicationConfiguration;
13+
import org.springframework.http.HttpHeaders;
14+
import org.springframework.http.HttpMethod;
15+
import org.springframework.http.HttpStatus;
16+
import org.springframework.http.MediaType;
17+
import org.springframework.http.RequestEntity;
18+
import org.springframework.http.ResponseEntity;
19+
import org.springframework.util.LinkedMultiValueMap;
20+
import org.springframework.util.MultiValueMap;
21+
import org.springframework.web.client.HttpStatusCodeException;
22+
import org.springframework.web.client.RestTemplate;
23+
24+
import sparklr.common.AbstractClientCredentialsProviderTests;
25+
26+
/**
27+
* Integration tests using the {@link HardCodedAuthenticationFilter}.
28+
*
29+
* One client should be able to use the token endpoint /oauth/token by only providing its client_id as a parameter.
30+
*
31+
* @author michaeltecourt
32+
*/
33+
@SpringApplicationConfiguration(classes = Application.class)
34+
public class ClientCredentialsProviderTests extends AbstractClientCredentialsProviderTests {
35+
36+
protected URI tokenUri;
37+
38+
@Before
39+
public void setUp() {
40+
tokenUri = URI.create(http.getUrl("/oauth/token"));
41+
}
42+
43+
/**
44+
* No Basic authentication provided, only the hard coded client_id.
45+
*/
46+
@Test
47+
@SuppressWarnings({ "unchecked", "rawtypes" })
48+
public void testHardCodedAuthenticationFineClient() {
49+
50+
RestTemplate restTemplate = new RestTemplate();
51+
MultiValueMap<String, String> params = new LinkedMultiValueMap<String, String>();
52+
params.add("grant_type", "client_credentials");
53+
params.add("client_id", "my-client-with-secret");
54+
HttpHeaders headers = new HttpHeaders();
55+
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
56+
RequestEntity<MultiValueMap<String, String>> req = new RequestEntity<MultiValueMap<String, String>>(params,
57+
headers, HttpMethod.POST, tokenUri);
58+
59+
ResponseEntity<Map> response = restTemplate.exchange(req, Map.class);
60+
assertEquals(HttpStatus.OK, response.getStatusCode());
61+
Map<String, String> body = response.getBody();
62+
String accessToken = body.get("access_token");
63+
assertNotNull(accessToken);
64+
}
65+
66+
@Test
67+
public void testHardCodedAuthenticationWrongClient() {
68+
69+
RestTemplate restTemplate = new RestTemplate();
70+
MultiValueMap<String, String> params = new LinkedMultiValueMap<String, String>();
71+
params.add("grant_type", "client_credentials");
72+
params.add("client_id", "my-trusted-client");
73+
HttpHeaders headers = new HttpHeaders();
74+
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
75+
RequestEntity<MultiValueMap<String, String>> req = new RequestEntity<MultiValueMap<String, String>>(params,
76+
headers, HttpMethod.POST, tokenUri);
77+
78+
try {
79+
restTemplate.exchange(req, Map.class);
80+
fail("Expected HTTP 401");
81+
}
82+
catch (HttpStatusCodeException e) {
83+
assertEquals(HttpStatus.UNAUTHORIZED, e.getStatusCode());
84+
}
85+
}
86+
}

tests/annotation/pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
<module>multi</module>
2020
<module>client</module>
2121
<module>resource</module>
22+
<module>custom-authentication</module>
2223
</modules>
2324

2425
<name>spring-oauth2-tests</name>

0 commit comments

Comments
 (0)