Skip to content

Commit 09a363b

Browse files
Patryk Kostrzewajgrandja
Patryk Kostrzewa
authored andcommitted
Implement Client Credentials Authentication
Fixes gh-39
1 parent 18b09af commit 09a363b

File tree

7 files changed

+529
-7
lines changed

7 files changed

+529
-7
lines changed

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

+37-2
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,52 @@
1818
import org.springframework.security.authentication.AuthenticationProvider;
1919
import org.springframework.security.core.Authentication;
2020
import org.springframework.security.core.AuthenticationException;
21+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
22+
import org.springframework.security.oauth2.core.OAuth2Error;
23+
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
24+
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
2125
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
26+
import org.springframework.util.Assert;
2227

2328
/**
29+
* An {@link AuthenticationProvider} implementation that validates {@link OAuth2ClientAuthenticationToken}s.
30+
*
2431
* @author Joe Grandja
32+
* @author Patryk Kostrzewa
2533
*/
2634
public class OAuth2ClientAuthenticationProvider implements AuthenticationProvider {
27-
private RegisteredClientRepository registeredClientRepository;
35+
private final RegisteredClientRepository registeredClientRepository;
36+
37+
/**
38+
* @param registeredClientRepository
39+
* the bean to lookup the client details from
40+
*/
41+
public OAuth2ClientAuthenticationProvider(RegisteredClientRepository registeredClientRepository) {
42+
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
43+
this.registeredClientRepository = registeredClientRepository;
44+
}
2845

2946
@Override
3047
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
31-
return authentication;
48+
String clientId = authentication.getName();
49+
if (authentication.getCredentials() == null) {
50+
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
51+
}
52+
53+
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
54+
// https://tools.ietf.org/html/rfc6749#section-2.4
55+
if (registeredClient == null) {
56+
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
57+
}
58+
59+
String presentedSecret = authentication.getCredentials()
60+
.toString();
61+
if (!registeredClient.getClientSecret()
62+
.equals(presentedSecret)) {
63+
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
64+
}
65+
66+
return new OAuth2ClientAuthenticationToken(registeredClient);
3267
}
3368

3469
@Override

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@
1818
import org.springframework.security.authentication.AbstractAuthenticationToken;
1919
import org.springframework.security.core.SpringSecurityCoreVersion;
2020
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
21-
2221
import java.util.Collections;
2322

2423
/**
2524
* @author Joe Grandja
25+
* @author Patryk Kostrzewa
2626
*/
2727
public class OAuth2ClientAuthenticationToken extends AbstractAuthenticationToken {
2828
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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 org.springframework.security.oauth2.server.authorization.web;
17+
18+
import org.springframework.http.HttpHeaders;
19+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
20+
import org.springframework.security.oauth2.core.OAuth2Error;
21+
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
22+
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
23+
import org.springframework.security.web.authentication.AuthenticationConverter;
24+
import org.springframework.util.StringUtils;
25+
import javax.servlet.http.HttpServletRequest;
26+
import java.nio.charset.StandardCharsets;
27+
import java.util.Base64;
28+
29+
/**
30+
* Converts from {@link HttpServletRequest} to {@link OAuth2ClientAuthenticationToken} that can be authenticated.
31+
*
32+
* @author Patryk Kostrzewa
33+
*/
34+
public class DefaultOAuth2ClientAuthenticationConverter implements AuthenticationConverter {
35+
36+
private static final String AUTHENTICATION_SCHEME_BASIC = "Basic";
37+
38+
@Override
39+
public OAuth2ClientAuthenticationToken convert(HttpServletRequest request) {
40+
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
41+
42+
if (header == null) {
43+
return null;
44+
}
45+
46+
header = header.trim();
47+
if (!StringUtils.startsWithIgnoreCase(header, AUTHENTICATION_SCHEME_BASIC)) {
48+
return null;
49+
}
50+
51+
if (header.equalsIgnoreCase(AUTHENTICATION_SCHEME_BASIC)) {
52+
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST));
53+
}
54+
55+
byte[] decoded;
56+
try {
57+
byte[] base64Token = header.substring(6)
58+
.getBytes(StandardCharsets.UTF_8);
59+
decoded = Base64.getDecoder()
60+
.decode(base64Token);
61+
} catch (IllegalArgumentException e) {
62+
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST));
63+
}
64+
65+
String token = new String(decoded, StandardCharsets.UTF_8);
66+
String[] credentials = token.split(":");
67+
if (credentials.length != 2) {
68+
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN));
69+
}
70+
return new OAuth2ClientAuthenticationToken(credentials[0], credentials[1]);
71+
}
72+
}

core/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilter.java

+131-4
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,21 @@
1515
*/
1616
package org.springframework.security.oauth2.server.authorization.web;
1717

18+
import org.springframework.http.MediaType;
19+
import org.springframework.http.server.ServletServerHttpResponse;
1820
import org.springframework.security.authentication.AuthenticationManager;
21+
import org.springframework.security.core.Authentication;
22+
import org.springframework.security.core.AuthenticationException;
23+
import org.springframework.security.core.context.SecurityContextHolder;
24+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
25+
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
26+
import org.springframework.security.web.authentication.AuthenticationConverter;
27+
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
28+
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
29+
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
30+
import org.springframework.security.web.util.matcher.RequestMatcher;
31+
import org.springframework.util.Assert;
1932
import org.springframework.web.filter.OncePerRequestFilter;
20-
2133
import javax.servlet.FilterChain;
2234
import javax.servlet.ServletException;
2335
import javax.servlet.http.HttpServletRequest;
@@ -26,15 +38,130 @@
2638

2739
/**
2840
* @author Joe Grandja
41+
* @author Patryk Kostrzewa
2942
*/
3043
public class OAuth2ClientAuthenticationFilter extends OncePerRequestFilter {
31-
private AuthenticationManager authenticationManager;
44+
45+
public static final String DEFAULT_FILTER_PROCESSES_URL = "/oauth2/token";
46+
private final AuthenticationManager authenticationManager;
47+
private final RequestMatcher requestMatcher;
48+
private final OAuth2ErrorHttpMessageConverter errorMessageConverter = new OAuth2ErrorHttpMessageConverter();
49+
private AuthenticationSuccessHandler authenticationSuccessHandler;
50+
private AuthenticationFailureHandler authenticationFailureHandler;
51+
private AuthenticationConverter authenticationConverter = new DefaultOAuth2ClientAuthenticationConverter();
52+
53+
/**
54+
* Creates an instance which will authenticate against the supplied
55+
* {@code AuthenticationManager}.
56+
*
57+
* @param authenticationManager
58+
* the bean to submit authentication requests to
59+
*/
60+
public OAuth2ClientAuthenticationFilter(AuthenticationManager authenticationManager) {
61+
this(authenticationManager, DEFAULT_FILTER_PROCESSES_URL);
62+
}
63+
64+
/**
65+
* Creates an instance which will authenticate against the supplied
66+
* {@code AuthenticationManager}.
67+
*
68+
* <p>
69+
* Configures default {@link RequestMatcher} verifying the provided endpoint.
70+
*
71+
* @param authenticationManager
72+
* the bean to submit authentication requests to
73+
* @param filterProcessesUrl
74+
* the filterProcessesUrl to match request URI against
75+
*/
76+
public OAuth2ClientAuthenticationFilter(AuthenticationManager authenticationManager, String filterProcessesUrl) {
77+
this(authenticationManager, new AntPathRequestMatcher(filterProcessesUrl, "POST"));
78+
}
79+
80+
/**
81+
* Creates an instance which will authenticate against the supplied
82+
* {@code AuthenticationManager} and custom {@code RequestMatcher}.
83+
*
84+
* @param authenticationManager
85+
* the bean to submit authentication requests to
86+
* @param requestMatcher
87+
* the {@code RequestMatcher} to match {@code HttpServletRequest} against
88+
*/
89+
public OAuth2ClientAuthenticationFilter(AuthenticationManager authenticationManager,
90+
RequestMatcher requestMatcher) {
91+
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
92+
Assert.notNull(requestMatcher, "requestMatcher cannot be null");
93+
this.authenticationManager = authenticationManager;
94+
this.requestMatcher = requestMatcher;
95+
this.authenticationSuccessHandler = this::defaultAuthenticationSuccessHandler;
96+
this.authenticationFailureHandler = this::defaultAuthenticationFailureHandler;
97+
}
3298

3399
@Override
34-
protected void doFilterInternal(HttpServletRequest request,
35-
HttpServletResponse response, FilterChain filterChain)
100+
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
36101
throws ServletException, IOException {
37102

103+
if (this.requestMatcher.matches(request)) {
104+
Authentication authentication = this.authenticationConverter.convert(request);
105+
if (authentication == null) {
106+
filterChain.doFilter(request, response);
107+
return;
108+
}
109+
try {
110+
final Authentication result = this.authenticationManager.authenticate(authentication);
111+
this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, result);
112+
} catch (OAuth2AuthenticationException failed) {
113+
this.authenticationFailureHandler.onAuthenticationFailure(request, response, failed);
114+
return;
115+
}
116+
}
117+
filterChain.doFilter(request, response);
118+
}
119+
120+
/**
121+
* Used to define custom behaviour on a successful authentication.
122+
*
123+
* @param authenticationSuccessHandler
124+
* the handler to be used
125+
*/
126+
public final void setAuthenticationSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler) {
127+
Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null");
128+
this.authenticationSuccessHandler = authenticationSuccessHandler;
38129
}
39130

131+
/**
132+
* Used to define custom behaviour on a failed authentication.
133+
*
134+
* @param authenticationFailureHandler
135+
* the handler to be used
136+
*/
137+
public final void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
138+
Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null");
139+
this.authenticationFailureHandler = authenticationFailureHandler;
140+
}
141+
142+
/**
143+
* Used to define custom {@link AuthenticationConverter}.
144+
*
145+
* @param authenticationConverter
146+
* the converter to be used
147+
*/
148+
public final void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
149+
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
150+
this.authenticationConverter = authenticationConverter;
151+
}
152+
153+
private void defaultAuthenticationSuccessHandler(HttpServletRequest request, HttpServletResponse response,
154+
Authentication authentication) {
155+
156+
SecurityContextHolder.getContext()
157+
.setAuthentication(authentication);
158+
}
159+
160+
private void defaultAuthenticationFailureHandler(HttpServletRequest request, HttpServletResponse response,
161+
AuthenticationException failed) throws IOException {
162+
163+
SecurityContextHolder.clearContext();
164+
this.errorMessageConverter.write(((OAuth2AuthenticationException) failed).getError(),
165+
MediaType.APPLICATION_JSON, new ServletServerHttpResponse(response));
166+
}
40167
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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 org.springframework.security.oauth2.server.authorization.authentication;
17+
18+
import org.junit.Before;
19+
import org.junit.Test;
20+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
21+
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
22+
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
23+
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
24+
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
25+
import java.util.Collections;
26+
import static org.assertj.core.api.Assertions.assertThat;
27+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
28+
29+
/**
30+
* Tests for {@link OAuth2ClientAuthenticationProvider}.
31+
*
32+
* @author Patryk Kostrzewa
33+
*/
34+
public class OAuth2ClientAuthenticationProviderTests {
35+
36+
private RegisteredClient registeredClient;
37+
private RegisteredClientRepository registeredClientRepository;
38+
private OAuth2ClientAuthenticationProvider authenticationProvider;
39+
40+
@Before
41+
public void setUp() {
42+
this.registeredClient = TestRegisteredClients.registeredClient()
43+
.build();
44+
this.registeredClientRepository = new InMemoryRegisteredClientRepository(this.registeredClient);
45+
this.authenticationProvider = new OAuth2ClientAuthenticationProvider(this.registeredClientRepository);
46+
}
47+
48+
@Test
49+
public void constructorWhenRegisteredClientRepositoryIsNullThenThrowIllegalArgumentException() {
50+
assertThatThrownBy(() -> new OAuth2ClientAuthenticationProvider(null)).isInstanceOf(
51+
IllegalArgumentException.class);
52+
}
53+
54+
@Test
55+
public void supportsWhenTypeOAuth2ClientAuthenticationTokenThenReturnTrue() {
56+
assertThat(this.authenticationProvider.supports(OAuth2ClientAuthenticationToken.class)).isTrue();
57+
}
58+
59+
@Test
60+
public void authenticateWhenNullCredentialsThenThrowOAuth2AuthorizationException() {
61+
assertThatThrownBy(() -> {
62+
this.authenticationProvider.authenticate(new OAuth2ClientAuthenticationToken("id", null));
63+
}).isInstanceOf(OAuth2AuthenticationException.class);
64+
}
65+
66+
@Test
67+
public void authenticateWhenNullRegisteredClientThenThrowOAuth2AuthorizationException() {
68+
assertThatThrownBy(() -> {
69+
this.authenticationProvider.authenticate(new OAuth2ClientAuthenticationToken("id", "secret"));
70+
}).isInstanceOf(OAuth2AuthenticationException.class);
71+
}
72+
73+
@Test
74+
public void authenticateWhenCredentialsNotEqualThenThrowOAuth2AuthorizationException() {
75+
assertThatThrownBy(() -> {
76+
this.authenticationProvider.authenticate(
77+
new OAuth2ClientAuthenticationToken(this.registeredClient.getClientId(),
78+
this.registeredClient.getClientSecret() + "_invalid"));
79+
}).isInstanceOf(OAuth2AuthenticationException.class);
80+
}
81+
82+
@Test
83+
public void authenticateWhenAuthenticationSuccessResponseThenReturnClientAuthenticationToken() {
84+
OAuth2ClientAuthenticationToken authenticationResult = (OAuth2ClientAuthenticationToken) this.authenticationProvider.authenticate(
85+
new OAuth2ClientAuthenticationToken(this.registeredClient.getClientId(),
86+
registeredClient.getClientSecret()));
87+
assertThat(authenticationResult.isAuthenticated()).isTrue();
88+
assertThat(authenticationResult.getAuthorities()).isEqualTo(Collections.emptyList());
89+
}
90+
}

0 commit comments

Comments
 (0)