Skip to content

Commit faad0be

Browse files
committed
Add test for refresh_token grant with public client
Related gh-1432
1 parent e76fde8 commit faad0be

File tree

1 file changed

+160
-1
lines changed
  • oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers

1 file changed

+160
-1
lines changed

oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2RefreshTokenGrantTests.java

Lines changed: 160 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2022 the original author or authors.
2+
* Copyright 2020-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,6 +23,8 @@
2323
import java.util.List;
2424
import java.util.Set;
2525

26+
import jakarta.servlet.http.HttpServletRequest;
27+
2628
import com.nimbusds.jose.jwk.JWKSet;
2729
import com.nimbusds.jose.jwk.source.JWKSource;
2830
import com.nimbusds.jose.proc.SecurityContext;
@@ -34,6 +36,7 @@
3436

3537
import org.springframework.beans.factory.annotation.Autowired;
3638
import org.springframework.context.annotation.Bean;
39+
import org.springframework.context.annotation.Configuration;
3740
import org.springframework.context.annotation.Import;
3841
import org.springframework.http.HttpHeaders;
3942
import org.springframework.http.HttpStatus;
@@ -43,16 +46,25 @@
4346
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
4447
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
4548
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
49+
import org.springframework.lang.Nullable;
4650
import org.springframework.mock.http.client.MockClientHttpResponse;
4751
import org.springframework.mock.web.MockHttpServletResponse;
52+
import org.springframework.security.authentication.AuthenticationProvider;
4853
import org.springframework.security.authentication.TestingAuthenticationToken;
54+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
4955
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
5056
import org.springframework.security.core.Authentication;
57+
import org.springframework.security.core.AuthenticationException;
5158
import org.springframework.security.core.GrantedAuthority;
59+
import org.springframework.security.core.Transient;
5260
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
5361
import org.springframework.security.crypto.password.PasswordEncoder;
5462
import org.springframework.security.oauth2.core.AuthorizationGrantType;
63+
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
5564
import org.springframework.security.oauth2.core.OAuth2AccessToken;
65+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
66+
import org.springframework.security.oauth2.core.OAuth2Error;
67+
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
5668
import org.springframework.security.oauth2.core.OAuth2Token;
5769
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
5870
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
@@ -66,6 +78,7 @@
6678
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
6779
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
6880
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
81+
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
6982
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
7083
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository.RegisteredClientParametersMapper;
7184
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
@@ -77,10 +90,15 @@
7790
import org.springframework.security.oauth2.server.authorization.test.SpringTestContextExtension;
7891
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
7992
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
93+
import org.springframework.security.web.SecurityFilterChain;
94+
import org.springframework.security.web.authentication.AuthenticationConverter;
95+
import org.springframework.security.web.util.matcher.RequestMatcher;
8096
import org.springframework.test.web.servlet.MockMvc;
8197
import org.springframework.test.web.servlet.MvcResult;
98+
import org.springframework.util.Assert;
8299
import org.springframework.util.LinkedMultiValueMap;
83100
import org.springframework.util.MultiValueMap;
101+
import org.springframework.util.StringUtils;
84102

85103
import static org.assertj.core.api.Assertions.assertThat;
86104
import static org.hamcrest.CoreMatchers.containsString;
@@ -217,6 +235,32 @@ public void requestWhenRevokeAndRefreshThenAccessTokenActive() throws Exception
217235
assertThat(accessToken.isActive()).isTrue();
218236
}
219237

238+
// gh-1430
239+
@Test
240+
public void requestWhenRefreshTokenRequestWithPublicClientThenReturnAccessTokenResponse() throws Exception {
241+
this.spring.register(AuthorizationServerConfigurationWithPublicClientAuthentication.class).autowire();
242+
243+
RegisteredClient registeredClient = TestRegisteredClients.registeredPublicClient()
244+
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
245+
.build();
246+
this.registeredClientRepository.save(registeredClient);
247+
248+
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
249+
this.authorizationService.save(authorization);
250+
251+
this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
252+
.params(getRefreshTokenRequestParameters(authorization))
253+
.param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId()))
254+
.andExpect(status().isOk())
255+
.andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store")))
256+
.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache")))
257+
.andExpect(jsonPath("$.access_token").isNotEmpty())
258+
.andExpect(jsonPath("$.token_type").isNotEmpty())
259+
.andExpect(jsonPath("$.expires_in").isNotEmpty())
260+
.andExpect(jsonPath("$.refresh_token").isNotEmpty())
261+
.andExpect(jsonPath("$.scope").isNotEmpty());
262+
}
263+
220264
private static MultiValueMap<String, String> getRefreshTokenRequestParameters(OAuth2Authorization authorization) {
221265
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
222266
parameters.set(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.REFRESH_TOKEN.getValue());
@@ -307,4 +351,119 @@ static class ParametersMapper extends JdbcOAuth2AuthorizationService.OAuth2Autho
307351
}
308352

309353
}
354+
355+
@EnableWebSecurity
356+
@Configuration(proxyBeanMethods = false)
357+
static class AuthorizationServerConfigurationWithPublicClientAuthentication extends AuthorizationServerConfiguration {
358+
// @formatter:off
359+
@Bean
360+
SecurityFilterChain authorizationServerSecurityFilterChain(
361+
HttpSecurity http, RegisteredClientRepository registeredClientRepository) throws Exception {
362+
363+
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
364+
new OAuth2AuthorizationServerConfigurer();
365+
authorizationServerConfigurer
366+
.clientAuthentication(clientAuthentication ->
367+
clientAuthentication
368+
.authenticationConverter(
369+
new PublicClientRefreshTokenAuthenticationConverter())
370+
.authenticationProvider(
371+
new PublicClientRefreshTokenAuthenticationProvider(registeredClientRepository))
372+
);
373+
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
374+
375+
http
376+
.securityMatcher(endpointsMatcher)
377+
.authorizeHttpRequests(authorize ->
378+
authorize.anyRequest().authenticated()
379+
)
380+
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
381+
.apply(authorizationServerConfigurer);
382+
return http.build();
383+
}
384+
// @formatter:on
385+
}
386+
387+
@Transient
388+
private static final class PublicClientRefreshTokenAuthenticationToken extends OAuth2ClientAuthenticationToken {
389+
390+
private PublicClientRefreshTokenAuthenticationToken(String clientId) {
391+
super(clientId, ClientAuthenticationMethod.NONE, null, null);
392+
}
393+
394+
private PublicClientRefreshTokenAuthenticationToken(RegisteredClient registeredClient) {
395+
super(registeredClient, ClientAuthenticationMethod.NONE, null);
396+
}
397+
398+
}
399+
400+
private static final class PublicClientRefreshTokenAuthenticationConverter implements AuthenticationConverter {
401+
402+
@Nullable
403+
@Override
404+
public Authentication convert(HttpServletRequest request) {
405+
// grant_type (REQUIRED)
406+
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
407+
if (!AuthorizationGrantType.REFRESH_TOKEN.getValue().equals(grantType)) {
408+
return null;
409+
}
410+
411+
// client_id (REQUIRED)
412+
String clientId = request.getParameter(OAuth2ParameterNames.CLIENT_ID);
413+
if (!StringUtils.hasText(clientId)) {
414+
return null;
415+
}
416+
417+
return new PublicClientRefreshTokenAuthenticationToken(clientId);
418+
}
419+
420+
}
421+
422+
private static final class PublicClientRefreshTokenAuthenticationProvider implements AuthenticationProvider {
423+
private final RegisteredClientRepository registeredClientRepository;
424+
425+
private PublicClientRefreshTokenAuthenticationProvider(RegisteredClientRepository registeredClientRepository) {
426+
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
427+
this.registeredClientRepository = registeredClientRepository;
428+
}
429+
430+
@Override
431+
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
432+
PublicClientRefreshTokenAuthenticationToken publicClientAuthentication =
433+
(PublicClientRefreshTokenAuthenticationToken) authentication;
434+
435+
if (!ClientAuthenticationMethod.NONE.equals(publicClientAuthentication.getClientAuthenticationMethod())) {
436+
return null;
437+
}
438+
439+
String clientId = publicClientAuthentication.getPrincipal().toString();
440+
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
441+
if (registeredClient == null) {
442+
throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);
443+
}
444+
445+
if (!registeredClient.getClientAuthenticationMethods().contains(
446+
publicClientAuthentication.getClientAuthenticationMethod())) {
447+
throwInvalidClient("authentication_method");
448+
}
449+
450+
return new PublicClientRefreshTokenAuthenticationToken(registeredClient);
451+
}
452+
453+
@Override
454+
public boolean supports(Class<?> authentication) {
455+
return PublicClientRefreshTokenAuthenticationToken.class.isAssignableFrom(authentication);
456+
}
457+
458+
private static void throwInvalidClient(String parameterName) {
459+
OAuth2Error error = new OAuth2Error(
460+
OAuth2ErrorCodes.INVALID_CLIENT,
461+
"Public client authentication failed: " + parameterName,
462+
null
463+
);
464+
throw new OAuth2AuthenticationException(error);
465+
}
466+
467+
}
468+
310469
}

0 commit comments

Comments
 (0)