Skip to content

Commit 18b09af

Browse files
Tom van den Bergejgrandja
Tom van den Berge
authored andcommitted
Add client credentials authentication filter
Fixes gh-5
1 parent 4c8f89a commit 18b09af

File tree

4 files changed

+219
-2
lines changed

4 files changed

+219
-2
lines changed

core/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationToken.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@ public OAuth2ClientAuthenticationToken(RegisteredClient registeredClient) {
4444

4545
@Override
4646
public Object getCredentials() {
47-
return null;
47+
return this.clientSecret;
4848
}
4949

5050
@Override
5151
public Object getPrincipal() {
52-
return null;
52+
return this.clientId;
5353
}
5454
}

samples/boot/minimal/spring-authorization-server-samples-boot-minimal.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ dependencies {
44
implementation 'org.springframework.boot:spring-boot-starter-web'
55
implementation 'org.springframework.boot:spring-boot-starter-security'
66
implementation 'com.nimbusds:oauth2-oidc-sdk'
7+
implementation project(':spring-authorization-server-core')
78

89
testImplementation('org.springframework.boot:spring-boot-starter-test') {
910
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Copyright 2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package sample;
17+
18+
import org.springframework.http.HttpMethod;
19+
import org.springframework.security.authentication.AuthenticationManager;
20+
import org.springframework.security.authentication.BadCredentialsException;
21+
import org.springframework.security.core.Authentication;
22+
import org.springframework.security.core.context.SecurityContextHolder;
23+
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
24+
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
25+
import org.springframework.security.web.util.matcher.RequestMatcher;
26+
import org.springframework.web.filter.OncePerRequestFilter;
27+
28+
import javax.servlet.FilterChain;
29+
import javax.servlet.ServletException;
30+
import javax.servlet.http.HttpServletRequest;
31+
import javax.servlet.http.HttpServletResponse;
32+
import java.io.IOException;
33+
import java.nio.charset.Charset;
34+
import java.util.Base64;
35+
36+
import static java.nio.charset.StandardCharsets.UTF_8;
37+
38+
/**
39+
* A filter to perform client authentication for the Token Endpoint.
40+
*
41+
* See <a href="https://tools.ietf.org/html/rfc6749#section-2.3.1">RFC-6749 2.3.1</a>.
42+
*/
43+
public class ClientCredentialsAuthenticationFilter extends OncePerRequestFilter {
44+
private final AuthenticationManager authenticationManager;
45+
private final RequestMatcher requestMatcher = new AntPathRequestMatcher("/oauth2/token", HttpMethod.POST.name());
46+
47+
public ClientCredentialsAuthenticationFilter(AuthenticationManager authenticationManager) {
48+
this.authenticationManager = authenticationManager;
49+
}
50+
51+
@Override
52+
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
53+
throws ServletException, IOException {
54+
55+
if (this.requestMatcher.matches(request)) {
56+
String[] credentials = extractBasicAuthenticationCredentials(request);
57+
String clientId = credentials[0];
58+
String clientSecret = credentials[1];
59+
60+
OAuth2ClientAuthenticationToken authenticationToken = new OAuth2ClientAuthenticationToken(clientId, clientSecret);
61+
62+
Authentication authentication = this.authenticationManager.authenticate(authenticationToken);
63+
64+
SecurityContextHolder.getContext().setAuthentication(authentication);
65+
}
66+
67+
chain.doFilter(request, response);
68+
}
69+
70+
private String[] extractBasicAuthenticationCredentials(HttpServletRequest request) {
71+
String header = request.getHeader("Authorization");
72+
if (header != null && header.toLowerCase().startsWith("basic ")) {
73+
return extractAndDecodeHeader(header, request);
74+
}
75+
throw new BadCredentialsException("Missing basic authentication header");
76+
}
77+
78+
// Taken from BasicAuthenticationFilter (spring-security-web)
79+
private String[] extractAndDecodeHeader(String header, HttpServletRequest request) {
80+
81+
byte[] base64Token = header.substring(6).getBytes(UTF_8);
82+
byte[] decoded;
83+
try {
84+
decoded = Base64.getDecoder().decode(base64Token);
85+
}
86+
catch (IllegalArgumentException e) {
87+
throw new BadCredentialsException("Failed to decode basic authentication token");
88+
}
89+
90+
String token = new String(decoded, getCredentialsCharset(request));
91+
92+
int delim = token.indexOf(":");
93+
94+
if (delim == -1) {
95+
throw new BadCredentialsException("Invalid basic authentication token");
96+
}
97+
return new String[] { token.substring(0, delim), token.substring(delim + 1) };
98+
}
99+
100+
protected Charset getCredentialsCharset(HttpServletRequest httpRequest) {
101+
return UTF_8;
102+
}
103+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* Copyright 2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package sample;
17+
18+
import org.junit.jupiter.api.BeforeEach;
19+
import org.junit.jupiter.api.Test;
20+
import org.springframework.mock.web.MockFilterChain;
21+
import org.springframework.mock.web.MockHttpServletRequest;
22+
import org.springframework.mock.web.MockHttpServletResponse;
23+
import org.springframework.mock.web.MockServletContext;
24+
import org.springframework.security.authentication.AuthenticationManager;
25+
import org.springframework.security.authentication.BadCredentialsException;
26+
import org.springframework.security.core.context.SecurityContextHolder;
27+
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
28+
import org.springframework.util.Assert;
29+
30+
import static java.net.URI.create;
31+
import static java.nio.charset.StandardCharsets.UTF_8;
32+
import static java.util.Base64.getEncoder;
33+
import static org.assertj.core.api.Assertions.assertThat;
34+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
35+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
36+
37+
public class ClientCredentialsAuthenticationFilterTests {
38+
private static final String CLIENT_ID = "myclientid";
39+
private static final String CLIENT_SECRET = "myclientsecret";
40+
private final AuthenticationManager authenticationManager = authentication -> {
41+
Assert.isInstanceOf(OAuth2ClientAuthenticationToken.class, authentication);
42+
OAuth2ClientAuthenticationToken token = (OAuth2ClientAuthenticationToken) authentication;
43+
if (CLIENT_ID.equals(token.getPrincipal()) && CLIENT_SECRET.equals(token.getCredentials())) {
44+
authentication.setAuthenticated(true);
45+
return authentication;
46+
}
47+
throw new BadCredentialsException("Bad credentials");
48+
};
49+
private final ClientCredentialsAuthenticationFilter filter = new ClientCredentialsAuthenticationFilter(this.authenticationManager);
50+
51+
@BeforeEach
52+
public void setup() {
53+
SecurityContextHolder.clearContext();
54+
}
55+
56+
@Test
57+
public void doFilterWhenUrlDoesNotMatchThenDontAuthenticate() throws Exception {
58+
MockHttpServletRequest request = post(create("/someotherendpoint")).buildRequest(new MockServletContext());
59+
request.addHeader("Authorization", basicAuthHeader(CLIENT_ID, CLIENT_SECRET));
60+
61+
filter.doFilter(request, new MockHttpServletResponse(), new MockFilterChain());
62+
63+
assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull();
64+
}
65+
66+
@Test
67+
public void doFilterWhenRequestMatchesThenAuthenticate() throws Exception {
68+
MockHttpServletRequest request = post(create("/oauth2/token")).buildRequest(new MockServletContext());
69+
request.addHeader("Authorization", basicAuthHeader(CLIENT_ID, CLIENT_SECRET));
70+
71+
filter.doFilter(request, new MockHttpServletResponse(), new MockFilterChain());
72+
73+
assertThat(SecurityContextHolder.getContext().getAuthentication().isAuthenticated()).isTrue();
74+
}
75+
76+
@Test
77+
public void doFilterWhenBasicAuthenticationHeaderIsMissingThenThrowBadCredentialsException() {
78+
MockHttpServletRequest request = post(create("/oauth2/token")).buildRequest(new MockServletContext());
79+
assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() ->
80+
filter.doFilter(request, new MockHttpServletResponse(), new MockFilterChain()));
81+
}
82+
83+
@Test
84+
public void doFilterWhenBasicAuthenticationHeaderHasInvalidSyntaxThenThrowBadCredentialsException() {
85+
MockHttpServletRequest request = post(create("/oauth2/token")).buildRequest(new MockServletContext());
86+
request.addHeader("Authorization", "Basic invalid");
87+
88+
assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() ->
89+
filter.doFilter(request, new MockHttpServletResponse(), new MockFilterChain()));
90+
}
91+
92+
@Test
93+
public void doFilterWhenBasicAuthenticationProvidesIncorrectSecretThenThrowBadCredentialsException() {
94+
MockHttpServletRequest request = post(create("/oauth2/token")).buildRequest(new MockServletContext());
95+
request.addHeader("Authorization", basicAuthHeader(CLIENT_ID, "incorrectsecret"));
96+
97+
assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() ->
98+
filter.doFilter(request, new MockHttpServletResponse(), new MockFilterChain()));
99+
}
100+
101+
@Test
102+
public void doFilterWhenBasicAuthenticationProvidesIncorrectClientIdThenThrowBadCredentialsException() {
103+
MockHttpServletRequest request = post(create("/oauth2/token")).buildRequest(new MockServletContext());
104+
request.addHeader("Authorization", basicAuthHeader("anotherclientid", CLIENT_SECRET));
105+
106+
assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() ->
107+
filter.doFilter(request, new MockHttpServletResponse(), new MockFilterChain()));
108+
}
109+
110+
private static String basicAuthHeader(String clientId, String clientSecret) {
111+
return "Basic " + getEncoder().encodeToString((clientId + ":" + clientSecret).getBytes(UTF_8));
112+
}
113+
}

0 commit comments

Comments
 (0)