-
Notifications
You must be signed in to change notification settings - Fork 1.3k
URL-encoded parameters in redirect URI are encoded twice #1011
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5063a98
5eb9458
1b1f4d3
ca9e58c
1dfa38c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -83,7 +83,7 @@ | |
public class OAuth2AuthorizationEndpointFilterTests { | ||
private static final String DEFAULT_AUTHORIZATION_ENDPOINT_URI = "/oauth2/authorize"; | ||
private static final String AUTHORIZATION_URI = "https://provider.com/oauth2/authorize"; | ||
private static final String STATE = "state"; | ||
private static final String STATE = "previously encoded/state"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please revert all changes related to the |
||
private static final String REMOTE_ADDRESS = "remote-address"; | ||
private AuthenticationManager authenticationManager; | ||
private OAuth2AuthorizationEndpointFilter filter; | ||
|
@@ -285,7 +285,7 @@ public void doFilterWhenAuthorizationRequestAuthenticationExceptionThenErrorResp | |
new OAuth2AuthorizationCodeRequestAuthenticationToken( | ||
AUTHORIZATION_URI, registeredClient.getClientId(), principal, | ||
registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), null); | ||
OAuth2Error error = new OAuth2Error("errorCode", "errorDescription", "errorUri"); | ||
OAuth2Error error = new OAuth2Error("errorCode", "errorDescription", "errorUri#section"); | ||
when(this.authenticationManager.authenticate(any())) | ||
.thenThrow(new OAuth2AuthorizationCodeRequestAuthenticationException(error, authorizationCodeRequestAuthentication)); | ||
|
||
|
@@ -299,7 +299,7 @@ public void doFilterWhenAuthorizationRequestAuthenticationExceptionThenErrorResp | |
verifyNoInteractions(filterChain); | ||
|
||
assertThat(response.getStatus()).isEqualTo(HttpStatus.FOUND.value()); | ||
assertThat(response.getRedirectedUrl()).isEqualTo("https://example.com?error=errorCode&error_description=errorDescription&error_uri=errorUri&state=state"); | ||
assertThat(response.getRedirectedUrl()).isEqualTo("https://example.com?error=errorCode&error_description=errorDescription&error_uri=errorUri%23section&state=previously%20encoded%2Fstate"); | ||
assertThat(SecurityContextHolder.getContext().getAuthentication()).isSameAs(this.principal); | ||
} | ||
|
||
|
@@ -459,7 +459,7 @@ public void doFilterWhenAuthorizationRequestConsentRequiredWithCustomConsentUriT | |
verifyNoInteractions(filterChain); | ||
|
||
assertThat(response.getStatus()).isEqualTo(HttpStatus.FOUND.value()); | ||
assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/oauth2/custom-consent?scope=scope1%20scope2&client_id=client-1&state=state"); | ||
assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/oauth2/custom-consent?scope=scope1%20scope2&client_id=client-1&state=previously%20encoded/state"); | ||
} | ||
|
||
@Test | ||
|
@@ -535,7 +535,7 @@ public void doFilterWhenAuthorizationRequestConsentRequiredWithPreviouslyApprove | |
|
||
@Test | ||
public void doFilterWhenAuthorizationRequestAuthenticatedThenAuthorizationResponse() throws Exception { | ||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); | ||
RegisteredClient registeredClient = TestRegisteredClients.registeredClientEncodedUri().build(); | ||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult = | ||
new OAuth2AuthorizationCodeRequestAuthenticationToken( | ||
AUTHORIZATION_URI, registeredClient.getClientId(), principal, this.authorizationCode, | ||
|
@@ -560,7 +560,7 @@ public void doFilterWhenAuthorizationRequestAuthenticatedThenAuthorizationRespon | |
.extracting(WebAuthenticationDetails::getRemoteAddress) | ||
.isEqualTo(REMOTE_ADDRESS); | ||
assertThat(response.getStatus()).isEqualTo(HttpStatus.FOUND.value()); | ||
assertThat(response.getRedirectedUrl()).isEqualTo("https://example.com?code=code&state=state"); | ||
assertThat(response.getRedirectedUrl()).isEqualTo("https://example.com?param=is%2Fencoded&code=code&state=previously%20encoded%2Fstate"); | ||
} | ||
|
||
@Test | ||
|
@@ -591,7 +591,7 @@ public void doFilterWhenAuthenticationRequestAuthenticatedThenAuthorizationRespo | |
verifyNoInteractions(filterChain); | ||
|
||
assertThat(response.getStatus()).isEqualTo(HttpStatus.FOUND.value()); | ||
assertThat(response.getRedirectedUrl()).isEqualTo("https://example.com?code=code&state=state"); | ||
assertThat(response.getRedirectedUrl()).isEqualTo("https://example.com?code=code&state=previously%20encoded%2Fstate"); | ||
} | ||
|
||
private void doFilterWhenAuthorizationRequestInvalidParameterThenError(RegisteredClient registeredClient, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,10 @@ group = project.rootProject.group | |
version = project.rootProject.version | ||
sourceCompatibility = "17" | ||
|
||
test { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please revert all changes in |
||
useJUnitPlatform() | ||
} | ||
|
||
repositories { | ||
mavenCentral() | ||
maven { url 'https://repo.spring.io/milestone' } | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,10 @@ group = project.rootProject.group | |
version = project.rootProject.version | ||
sourceCompatibility = "17" | ||
|
||
test { | ||
useJUnitPlatform() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be removed since it's automatically applied by the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I spent quite a while figuring out why the tests don't run locally and in CI, in my opinion this config should have been added in the "Upgrade to JUnit 5" commit. Did you try it out and are tests running (and failing the build) on your machine without explicitly enabling JUnit 5? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just realized that the samples do not use the However, I'm wondering why the tests are failing on your end? The tests in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, I didn't make myself fully clear. The tests are not failing in their current state. What I meant was: if Gradle says the build was successful, that does not necessarily mean that it ran tests. To check if it does, I like to make a test fail, e.g. by throwing a RuntimeException. The failing test should also lead to a failing build. For CI, I checked the logs of several commits for the test output – I think there's a DB-related message. This message did not appear after the upgrade to JUnit 5. |
||
} | ||
|
||
repositories { | ||
mavenCentral() | ||
maven { url 'https://repo.spring.io/milestone' } | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -87,6 +87,7 @@ public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTe | |
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) | ||
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) | ||
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc") | ||
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc?param=is%2Fencoded") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's change |
||
.redirectUri("http://127.0.0.1:8080/authorized") | ||
.scope(OidcScopes.OPENID) | ||
.scope(OidcScopes.PROFILE) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,9 @@ | |
package sample; | ||
|
||
import java.io.IOException; | ||
import java.net.URLDecoder; | ||
import java.net.URLEncoder; | ||
import java.nio.charset.StandardCharsets; | ||
|
||
import com.gargoylesoftware.htmlunit.Page; | ||
import com.gargoylesoftware.htmlunit.WebClient; | ||
|
@@ -33,6 +36,7 @@ | |
import org.springframework.boot.test.context.SpringBootTest; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.test.context.junit.jupiter.SpringExtension; | ||
import org.springframework.web.util.UriComponents; | ||
import org.springframework.web.util.UriComponentsBuilder; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
|
@@ -46,15 +50,18 @@ | |
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) | ||
@AutoConfigureMockMvc | ||
public class DefaultAuthorizationServerApplicationTests { | ||
private static final String REDIRECT_URI = "http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc"; | ||
private static final String REDIRECT_URI = "http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc?param=is%2Fencoded"; | ||
public static final String STATE = "some+%20encoded%2Fstate"; | ||
public static final String REENCODED_STATE = "some%20%20encoded%2Fstate"; | ||
|
||
private static final String AUTHORIZATION_REQUEST = UriComponentsBuilder | ||
.fromPath("/oauth2/authorize") | ||
.queryParam("response_type", "code") | ||
.queryParam("client_id", "messaging-client") | ||
.queryParam("scope", "openid") | ||
.queryParam("state", "some-state") | ||
.queryParam("redirect_uri", REDIRECT_URI) | ||
.queryParam("state", STATE) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please revert all changes related to the |
||
.queryParam("redirect_uri", URLEncoder.encode(REDIRECT_URI, StandardCharsets.UTF_8)) | ||
.build() | ||
.toUriString(); | ||
|
||
@Autowired | ||
|
@@ -67,6 +74,16 @@ public void setUp() { | |
this.webClient.getCookieManager().clearCookies(); // log out | ||
} | ||
|
||
@Test | ||
void authorizationRequestProperlyEncoded() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please remove this test since it directly tests |
||
UriComponents uriComponents = UriComponentsBuilder.fromUriString(AUTHORIZATION_REQUEST).build(); | ||
|
||
assertThat(uriComponents.getQueryParams().getFirst("state")).isEqualTo(STATE); | ||
|
||
String redirectUri = uriComponents.getQueryParams().getFirst("redirect_uri"); | ||
assertThat(URLDecoder.decode(redirectUri, StandardCharsets.UTF_8)).isEqualTo(REDIRECT_URI); | ||
} | ||
|
||
@Test | ||
public void whenLoginSuccessfulThenDisplayNotFoundError() throws IOException { | ||
HtmlPage page = this.webClient.getPage("/"); | ||
|
@@ -108,6 +125,10 @@ public void whenLoggingInAndRequestingTokenThenRedirectsToClientApplication() th | |
|
||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.MOVED_PERMANENTLY.value()); | ||
String location = response.getResponseHeaderValue("location"); | ||
|
||
String state = UriComponentsBuilder.fromHttpUrl(location).build().getQueryParams().getFirst("state"); | ||
assertThat(state).isEqualTo(REENCODED_STATE); | ||
|
||
assertThat(location).startsWith(REDIRECT_URI); | ||
assertThat(location).contains("code="); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,9 @@ | |
package sample; | ||
|
||
import java.io.IOException; | ||
import java.net.URLDecoder; | ||
import java.net.URLEncoder; | ||
import java.nio.charset.StandardCharsets; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
|
||
|
@@ -36,6 +39,7 @@ | |
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService; | ||
import org.springframework.security.test.context.support.WithMockUser; | ||
import org.springframework.test.context.junit.jupiter.SpringExtension; | ||
import org.springframework.web.util.UriComponents; | ||
import org.springframework.web.util.UriComponentsBuilder; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
|
@@ -58,15 +62,18 @@ public class DefaultAuthorizationServerConsentTests { | |
@MockBean | ||
private OAuth2AuthorizationConsentService authorizationConsentService; | ||
|
||
private final String redirectUri = "http://127.0.0.1/login/oauth2/code/messaging-client-oidc"; | ||
private final String redirectUri = "http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc?param=is%2Fencoded"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please revert all changes in |
||
public final String state = "some+%20encoded%2Fstate"; | ||
public final String reencodedState = "some%20%20encoded%2Fstate"; | ||
|
||
private final String authorizationRequestUri = UriComponentsBuilder | ||
.fromPath("/oauth2/authorize") | ||
.queryParam("response_type", "code") | ||
.queryParam("client_id", "messaging-client") | ||
.queryParam("scope", "openid message.read message.write") | ||
.queryParam("state", "state") | ||
.queryParam("redirect_uri", this.redirectUri) | ||
.queryParam("state", this.state) | ||
.queryParam("redirect_uri", URLEncoder.encode(this.redirectUri, StandardCharsets.UTF_8)) | ||
.build() | ||
.toUriString(); | ||
|
||
@BeforeEach | ||
|
@@ -77,6 +84,15 @@ public void setUp() { | |
when(this.authorizationConsentService.findById(any(), any())).thenReturn(null); | ||
} | ||
|
||
@Test | ||
void authorizationRequestProperlyEncoded() { | ||
UriComponents uriComponents = UriComponentsBuilder.fromUriString(authorizationRequestUri).build(); | ||
|
||
assertThat(uriComponents.getQueryParams().getFirst("state")).isEqualTo(state); | ||
String redirectUri = uriComponents.getQueryParams().getFirst("redirect_uri"); | ||
assertThat(URLDecoder.decode(redirectUri, StandardCharsets.UTF_8)).isEqualTo(this.redirectUri); | ||
} | ||
|
||
@Test | ||
@WithMockUser("user1") | ||
public void whenUserConsentsToAllScopesThenReturnAuthorizationCode() throws IOException { | ||
|
@@ -121,6 +137,9 @@ public void whenUserCancelsConsentThenReturnAccessDeniedError() throws IOExcepti | |
String location = cancelConsentResponse.getResponseHeaderValue("location"); | ||
assertThat(location).startsWith(this.redirectUri); | ||
assertThat(location).contains("error=access_denied"); | ||
|
||
String state = UriComponentsBuilder.fromHttpUrl(location).build().getQueryParams().getFirst("state"); | ||
assertThat(state).isEqualTo(reencodedState); | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove this and leverage the existing
TestRegisteredClients.registeredClient()
and add the url-encoded redirect-uri